mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-10-31 20:27:38 +08:00
fix(gateway): stamp Responses tool strictness only for Anthropic conversions
Native /v1/responses clients omit strict as API semantics; forcing false opted them out of strict tool schemas.
This commit is contained in:
@@ -9,6 +9,8 @@ type UpstreamRequest = {
|
||||
};
|
||||
|
||||
export type ResponsesToolStrictnessInput = {
|
||||
sourceAdapterKey?: string;
|
||||
sourceProvider?: string;
|
||||
targetProviderConfig?: {
|
||||
type?: string;
|
||||
};
|
||||
@@ -18,6 +20,8 @@ export type ResponsesToolStrictnessInput = {
|
||||
/**
|
||||
* OpenAI Responses treats omitted `strict` as "try to make this schema strict".
|
||||
* Claude Code tools leave `strict` unset, so optional fields get forced on.
|
||||
* Only converted Anthropic/Claude Code requests are stamped. Native
|
||||
* `/v1/responses` pass-through keeps omitted `strict` as API semantics.
|
||||
* Explicit boolean strictness is left alone. Other protocols pass through.
|
||||
*/
|
||||
export function applyResponsesToolStrictness(input: ResponsesToolStrictnessInput): UpstreamRequest {
|
||||
@@ -26,6 +30,11 @@ export function applyResponsesToolStrictness(input: ResponsesToolStrictnessInput
|
||||
if (providerType !== "openai_responses") {
|
||||
return upstreamRequest;
|
||||
}
|
||||
const sourceKey = input.sourceAdapterKey?.trim().toLowerCase();
|
||||
const sourceProvider = input.sourceProvider?.trim().toLowerCase();
|
||||
if (sourceKey !== "anthropic_messages" && sourceProvider !== "anthropic") {
|
||||
return upstreamRequest;
|
||||
}
|
||||
const body = upstreamRequest.body;
|
||||
if ((upstreamRequest.bodyEncoding ?? "json") !== "json" || !isRecord(body) || !Array.isArray(body.tools)) {
|
||||
return upstreamRequest;
|
||||
|
||||
@@ -42,6 +42,8 @@ function readTool(overrides = {}) {
|
||||
|
||||
function responsesInput(overrides = {}) {
|
||||
return {
|
||||
sourceAdapterKey: "anthropic_messages",
|
||||
sourceProvider: "anthropic",
|
||||
targetProviderConfig: {
|
||||
name: "multi-channel::openai_responses",
|
||||
type: "openai_responses"
|
||||
@@ -147,6 +149,49 @@ test("requests without tools pass through untouched", () => {
|
||||
assert.equal(result, input.upstreamRequest);
|
||||
});
|
||||
|
||||
test("native Responses requests leave omitted strict unchanged", () => {
|
||||
const tool = monitorTool();
|
||||
const body = {
|
||||
input: [],
|
||||
model: "gpt-5.1-codex",
|
||||
stream: true,
|
||||
tools: [tool]
|
||||
};
|
||||
|
||||
const input = responsesInput({
|
||||
sourceAdapterKey: undefined,
|
||||
sourceProvider: undefined,
|
||||
request: {
|
||||
body,
|
||||
headers: { "content-type": "application/json" }
|
||||
},
|
||||
upstreamRequest: {
|
||||
body,
|
||||
bodyEncoding: "json",
|
||||
headers: { "content-type": "application/json" },
|
||||
method: "POST",
|
||||
url: "https://provider.example/v1/responses"
|
||||
}
|
||||
});
|
||||
|
||||
const result = applyResponsesToolStrictness(input);
|
||||
|
||||
assert.equal(result, input.upstreamRequest);
|
||||
assert.equal(result.body.tools[0].strict, undefined);
|
||||
});
|
||||
|
||||
test("Chat Completions conversions leave omitted strict unchanged", () => {
|
||||
const input = responsesInput({
|
||||
sourceAdapterKey: "openai_chat",
|
||||
sourceProvider: "openai"
|
||||
});
|
||||
|
||||
const result = applyResponsesToolStrictness(input);
|
||||
|
||||
assert.equal(result, input.upstreamRequest);
|
||||
assert.equal(result.body.tools[0].strict, undefined);
|
||||
});
|
||||
|
||||
test("gateway boundary plugin registers the tool strictness hook", async () => {
|
||||
const hooks = createGatewayPlugin().providerHooks;
|
||||
assert.equal(hooks[0].key, "ccr-upstream-header-sanitizer");
|
||||
|
||||
Reference in New Issue
Block a user