mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 18:52:18 +03:00
fix(types): tighten chatCore helper contracts (#10175)
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { extractRequestToolIdentityMap } from "./chatCore/requestToolIdentity.ts";
|
||||
import {
|
||||
extractRequestToolIdentityMap,
|
||||
toToolNameAliasMap,
|
||||
} from "./chatCore/requestToolIdentity.ts";
|
||||
import { injectMemoryAndSkills } from "./chatCore/memorySkillsInjection.ts";
|
||||
import { resolveChatCoreRequestSetup } from "./chatCore/requestSetup.ts";
|
||||
import { normalizeOpenAICompatibleTools } from "./chatCore/openAICompatibleTools.ts";
|
||||
@@ -2317,13 +2320,8 @@ export async function handleChatCore({
|
||||
// response toolNameMap so the response translator can restore tool names
|
||||
// from their lowercased form (#9568). Only merge string-valued entries
|
||||
// (tool name aliases), not object-valued namespace identities (#7936).
|
||||
if (!toolNameMap && requestToolIdentityMap instanceof Map && requestToolIdentityMap.size > 0) {
|
||||
const hasStringValues = [...requestToolIdentityMap.values()].every(
|
||||
(v: unknown) => typeof v === "string"
|
||||
);
|
||||
if (hasStringValues) {
|
||||
toolNameMap = requestToolIdentityMap;
|
||||
}
|
||||
if (!toolNameMap) {
|
||||
toolNameMap = toToolNameAliasMap(requestToolIdentityMap);
|
||||
}
|
||||
delete translatedBody._toolNameMap;
|
||||
delete translatedBody._disableToolPrefix;
|
||||
|
||||
@@ -5,14 +5,27 @@
|
||||
* Extracted from handleChatCore's non-streaming success path: assemble the context object passed to
|
||||
* `guardrailRegistry.runPostCallHooks`. Pure value builder — no side effects, no early-returns. The
|
||||
* `disabledGuardrails` field is resolved via `resolveDisabledGuardrails` (injectable for tests).
|
||||
* Behaviour is byte-identical to the previous inline literal, including the `method: "POST"` /
|
||||
* `stream: false` constants and the headers/endpoint null-coalescing.
|
||||
* Preserves the previous field mapping and constants while narrowing values from
|
||||
* the untyped request boundary to the public guardrail contract.
|
||||
*/
|
||||
import { resolveDisabledGuardrails as defaultResolveDisabled } from "@/lib/guardrails";
|
||||
import {
|
||||
resolveDisabledGuardrails as defaultResolveDisabled,
|
||||
type GuardrailContext,
|
||||
} from "@/lib/guardrails";
|
||||
|
||||
type LoggerLike = unknown;
|
||||
type LoggerLike = GuardrailContext["log"];
|
||||
type HeadersLike = Headers | Record<string, unknown> | null;
|
||||
|
||||
function optionalRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function optionalString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
export function buildPostCallGuardrailContext(
|
||||
args: {
|
||||
apiKeyInfo: unknown;
|
||||
@@ -25,23 +38,24 @@ export function buildPostCallGuardrailContext(
|
||||
clientResponseFormat: unknown;
|
||||
},
|
||||
resolveDisabledGuardrails: typeof defaultResolveDisabled = defaultResolveDisabled
|
||||
) {
|
||||
): GuardrailContext {
|
||||
const headers = (args.clientRawRequest?.headers as HeadersLike) ?? null;
|
||||
const apiKeyInfo = optionalRecord(args.apiKeyInfo);
|
||||
return {
|
||||
apiKeyInfo: args.apiKeyInfo,
|
||||
apiKeyInfo,
|
||||
disabledGuardrails: resolveDisabledGuardrails({
|
||||
apiKeyInfo: (args.apiKeyInfo as Record<string, unknown> | null) ?? null,
|
||||
apiKeyInfo,
|
||||
body: args.body,
|
||||
headers,
|
||||
}),
|
||||
endpoint: args.clientRawRequest?.endpoint || null,
|
||||
endpoint: optionalString(args.clientRawRequest?.endpoint),
|
||||
headers,
|
||||
log: args.log,
|
||||
method: "POST",
|
||||
model: args.model,
|
||||
provider: args.provider,
|
||||
sourceFormat: args.responsePayloadFormat,
|
||||
sourceFormat: optionalString(args.responsePayloadFormat),
|
||||
stream: false,
|
||||
targetFormat: args.clientResponseFormat,
|
||||
} as const;
|
||||
targetFormat: optionalString(args.clientResponseFormat),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
type NamespaceIdentity = { namespace: string; name: string };
|
||||
export type NamespaceIdentity = { namespace: string; name: string };
|
||||
|
||||
/**
|
||||
* Return a string-valued copy only when the complete map is an alias ledger.
|
||||
*
|
||||
* The legacy `_toolNameMap` side channel can carry either response aliases or
|
||||
* namespace identities. Checking every value before copying keeps those two
|
||||
* contracts separate and gives callers a real `Map<string, string>` instead of
|
||||
* asserting an identity map into the alias shape.
|
||||
*/
|
||||
export function toToolNameAliasMap(
|
||||
map: ReadonlyMap<string, unknown> | null
|
||||
): Map<string, string> | null {
|
||||
if (!map || map.size === 0) return null;
|
||||
|
||||
const aliases = new Map<string, string>();
|
||||
for (const [wireName, originalName] of map) {
|
||||
if (typeof originalName !== "string") return null;
|
||||
aliases.set(wireName, originalName);
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the #7936 request-tool identity map from the translated body and
|
||||
|
||||
@@ -70,3 +70,20 @@ test("null apiKeyInfo coalesces to null for resolveDisabledGuardrails", () => {
|
||||
});
|
||||
assert.equal(received.apiKeyInfo, null);
|
||||
});
|
||||
|
||||
test("normalizes untyped context fields before returning the guardrail contract", () => {
|
||||
const ctx = buildPostCallGuardrailContext(
|
||||
baseArgs({
|
||||
apiKeyInfo: ["not", "a", "record"],
|
||||
clientRawRequest: { endpoint: 42 },
|
||||
responsePayloadFormat: { format: "openai" },
|
||||
clientResponseFormat: false,
|
||||
}),
|
||||
() => []
|
||||
);
|
||||
|
||||
assert.equal(ctx.apiKeyInfo, null);
|
||||
assert.equal(ctx.endpoint, null);
|
||||
assert.equal(ctx.sourceFormat, null);
|
||||
assert.equal(ctx.targetFormat, null);
|
||||
});
|
||||
|
||||
30
tests/unit/chatcore-request-tool-identity-contracts.test.ts
Normal file
30
tests/unit/chatcore-request-tool-identity-contracts.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { toToolNameAliasMap } from "../../open-sse/handlers/chatCore/requestToolIdentity.ts";
|
||||
|
||||
test("copies a string-valued request tool alias ledger", () => {
|
||||
const source = new Map<string, unknown>([["lowercase_tool", "OriginalTool"]]);
|
||||
|
||||
const aliases = toToolNameAliasMap(source);
|
||||
|
||||
assert.deepEqual(aliases, new Map([["lowercase_tool", "OriginalTool"]]));
|
||||
assert.notEqual(aliases, source);
|
||||
});
|
||||
|
||||
test("does not reinterpret namespace identities as response aliases", () => {
|
||||
const identities = new Map<string, unknown>([
|
||||
["mcp__files__read", { namespace: "mcp__files", name: "read" }],
|
||||
]);
|
||||
|
||||
assert.equal(toToolNameAliasMap(identities), null);
|
||||
});
|
||||
|
||||
test("rejects mixed alias and identity ledgers", () => {
|
||||
const mixed = new Map<string, unknown>([
|
||||
["plain", "PlainTool"],
|
||||
["mcp__files__read", { namespace: "mcp__files", name: "read" }],
|
||||
]);
|
||||
|
||||
assert.equal(toToolNameAliasMap(mixed), null);
|
||||
});
|
||||
Reference in New Issue
Block a user