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:
Gabriel Tavares
2026-08-30 01:19:28 +01:00
parent 54f57a35d5
commit 197a8a604f
2 changed files with 54 additions and 0 deletions
@@ -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");