From 197a8a604f9cfecd75aaeb59cd2ea02c2bac9d31 Mon Sep 17 00:00:00 2001 From: Gabriel Tavares Date: Sun, 30 Aug 2026 01:19:28 +0100 Subject: [PATCH] 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. --- .../core-runtime/responses-tool-strictness.ts | 9 ++++ .../responses-tool-strictness.test.mjs | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/packages/core/src/gateway/core-runtime/responses-tool-strictness.ts b/packages/core/src/gateway/core-runtime/responses-tool-strictness.ts index 1e49927f..9124d226 100644 --- a/packages/core/src/gateway/core-runtime/responses-tool-strictness.ts +++ b/packages/core/src/gateway/core-runtime/responses-tool-strictness.ts @@ -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; diff --git a/packages/core/test/unit/gateway/responses-tool-strictness.test.mjs b/packages/core/test/unit/gateway/responses-tool-strictness.test.mjs index 117931f8..6833ef11 100644 --- a/packages/core/test/unit/gateway/responses-tool-strictness.test.mjs +++ b/packages/core/test/unit/gateway/responses-tool-strictness.test.mjs @@ -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");