fix: align native openai transport defaults

This commit is contained in:
Peter Steinberger
2026-04-04 01:19:00 +01:00
parent bc16b9dccf
commit 628c71103e
12 changed files with 245 additions and 23 deletions

View File

@@ -58,7 +58,7 @@ describe("openai responses payload policy", () => {
expect(payload).not.toHaveProperty("prompt_cache_retention");
});
it("strips disabled reasoning payloads through the shared helper", () => {
it("keeps disabled reasoning payloads on native OpenAI responses routes", () => {
const payload = {
reasoning: {
effort: "none",
@@ -77,6 +77,32 @@ describe("openai responses payload policy", () => {
),
);
expect(payload).toEqual({
reasoning: {
effort: "none",
},
});
});
it("strips disabled reasoning payloads for proxy-like OpenAI responses routes", () => {
const payload = {
reasoning: {
effort: "none",
},
} satisfies Record<string, unknown>;
applyOpenAIResponsesPayloadPolicy(
payload,
resolveOpenAIResponsesPayloadPolicy(
{
api: "openai-responses",
provider: "openai",
baseUrl: "https://proxy.example.com/v1",
},
{ storeMode: "disable" },
),
);
expect(payload).not.toHaveProperty("reasoning");
});
});

View File

@@ -119,7 +119,8 @@ export function resolveOpenAIResponsesPayloadPolicy(
parsePositiveInteger(options.extraParams?.responsesCompactThreshold) ??
resolveOpenAIResponsesCompactThreshold(model),
explicitStore,
shouldStripDisabledReasoningPayload: capabilities.supportsOpenAIReasoningCompatPayload,
shouldStripDisabledReasoningPayload:
capabilities.supportsOpenAIReasoningCompatPayload && !capabilities.usesKnownNativeOpenAIRoute,
shouldStripPromptCache:
options.enablePromptCacheStripping === true && capabilities.shouldStripResponsesPromptCache,
shouldStripStore:

View File

@@ -439,6 +439,68 @@ describe("openai transport stream", () => {
expect(params.input?.[0]).toMatchObject({ role: "developer" });
});
it("defaults responses tool schemas to strict on native OpenAI routes", () => {
const params = buildOpenAIResponsesParams(
{
id: "gpt-5.4",
name: "GPT-5.4",
api: "openai-responses",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-responses">,
{
systemPrompt: "system",
messages: [],
tools: [
{
name: "lookup_weather",
description: "Get forecast",
parameters: { type: "object", properties: {} },
},
],
} as never,
undefined,
) as { tools?: Array<{ strict?: boolean }> };
expect(params.tools?.[0]?.strict).toBe(true);
});
it("omits responses strict tool shaping for proxy-like OpenAI routes", () => {
const params = buildOpenAIResponsesParams(
{
id: "custom-model",
name: "Custom Model",
api: "openai-responses",
provider: "openai",
baseUrl: "https://proxy.example.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-responses">,
{
systemPrompt: "system",
messages: [],
tools: [
{
name: "lookup_weather",
description: "Get forecast",
parameters: { type: "object", properties: {} },
},
],
} as never,
undefined,
) as { tools?: Array<{ strict?: boolean }> };
expect(params.tools?.[0]).not.toHaveProperty("strict");
});
it("gates responses service_tier to native OpenAI endpoints", () => {
const nativeParams = buildOpenAIResponsesParams(
{
@@ -695,6 +757,37 @@ describe("openai transport stream", () => {
expect(params.tools?.[0]?.function).not.toHaveProperty("strict");
});
it("defaults completions tool schemas to strict on native OpenAI routes", () => {
const params = buildOpenAICompletionsParams(
{
id: "gpt-5",
name: "GPT-5",
api: "openai-completions",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
{
systemPrompt: "system",
messages: [],
tools: [
{
name: "lookup_weather",
description: "Get forecast",
parameters: { type: "object", properties: {} },
},
],
} as never,
undefined,
) as { tools?: Array<{ function?: { strict?: boolean } }> };
expect(params.tools?.[0]?.function?.strict).toBe(true);
});
it("uses Mistral compat defaults for direct Mistral completions providers", () => {
const params = buildOpenAICompletionsParams(
{

View File

@@ -328,7 +328,15 @@ function convertResponsesTools(
tools: NonNullable<Context["tools"]>,
options?: { strict?: boolean | null },
): FunctionTool[] {
const strict = options?.strict === undefined ? false : options.strict;
const strict = options?.strict;
if (strict === undefined) {
return tools.map((tool) => ({
type: "function",
name: tool.name,
description: tool.description,
parameters: tool.parameters,
})) as unknown as FunctionTool[];
}
return tools.map((tool) => ({
type: "function",
name: tool.name,
@@ -698,7 +706,9 @@ export function buildOpenAIResponsesParams(
params.service_tier = options.serviceTier;
}
if (context.tools) {
params.tools = convertResponsesTools(context.tools);
params.tools = convertResponsesTools(context.tools, {
strict: resolveOpenAIStrictToolSetting(model as OpenAIModeModel),
});
}
if (model.reasoning) {
if (options?.reasoningEffort || options?.reasoningSummary) {
@@ -1156,14 +1166,56 @@ function mapReasoningEffort(effort: string, reasoningEffortMap: Record<string, s
return reasoningEffortMap[effort] ?? effort;
}
function convertTools(tools: NonNullable<Context["tools"]>, compat: ReturnType<typeof getCompat>) {
function resolvesToNativeOpenAIStrictTools(model: OpenAIModeModel): boolean {
const capabilities = resolveProviderRequestCapabilities({
provider: model.provider,
api: model.api,
baseUrl: model.baseUrl,
capability: "llm",
transport: "stream",
modelId: model.id,
compat:
model.compat && typeof model.compat === "object"
? (model.compat as { supportsStore?: boolean })
: undefined,
});
if (!capabilities.usesKnownNativeOpenAIRoute) {
return false;
}
return (
capabilities.provider === "openai" ||
capabilities.provider === "openai-codex" ||
capabilities.provider === "azure-openai" ||
capabilities.provider === "azure-openai-responses"
);
}
function resolveOpenAIStrictToolSetting(
model: OpenAIModeModel,
compat?: ReturnType<typeof getCompat>,
): boolean | undefined {
if (resolvesToNativeOpenAIStrictTools(model)) {
return true;
}
if (compat?.supportsStrictMode) {
return false;
}
return undefined;
}
function convertTools(
tools: NonNullable<Context["tools"]>,
compat: ReturnType<typeof getCompat>,
model: OpenAIModeModel,
) {
const strict = resolveOpenAIStrictToolSetting(model, compat);
return tools.map((tool) => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
...(compat.supportsStrictMode ? { strict: false } : {}),
...(strict === undefined ? {} : { strict }),
},
}));
}
@@ -1196,7 +1248,7 @@ export function buildOpenAICompletionsParams(
params.temperature = options.temperature;
}
if (context.tools) {
params.tools = convertTools(context.tools, compat);
params.tools = convertTools(context.tools, compat, model);
} else if (hasToolHistory(context.messages)) {
params.tools = [];
}

View File

@@ -707,7 +707,7 @@ describe("applyExtraParamsToAgent", () => {
expect(payloads[0]).not.toHaveProperty("reasoning_effort");
});
it("strips disabled reasoning payloads for native OpenAI responses routes", () => {
it("keeps disabled reasoning payloads for native OpenAI responses routes", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {
@@ -731,7 +731,9 @@ describe("applyExtraParamsToAgent", () => {
void agent.streamFn?.(model, context, {});
expect(payloads).toHaveLength(1);
expect(payloads[0]).not.toHaveProperty("reasoning");
expect(payloads[0]).toEqual({
reasoning: { effort: "none", summary: "auto" },
});
});
it("keeps disabled reasoning payloads for proxied OpenAI responses routes", () => {
@@ -1572,7 +1574,7 @@ describe("applyExtraParamsToAgent", () => {
expect(calls[0]?.transport).toBe("auto");
});
it("defaults OpenAI transport to auto without websocket warm-up", () => {
it("defaults OpenAI transport to auto with websocket warm-up", () => {
const { calls, agent } = createOptionsCaptureAgent();
applyExtraParamsToAgent(agent, undefined, "openai", "gpt-5");
@@ -1587,7 +1589,7 @@ describe("applyExtraParamsToAgent", () => {
expect(calls).toHaveLength(1);
expect(calls[0]?.transport).toBe("auto");
expect(calls[0]?.openaiWsWarmup).toBe(false);
expect(calls[0]?.openaiWsWarmup).toBe(true);
});
it("injects native Codex web_search for direct openai-codex Responses models", () => {
@@ -2165,7 +2167,7 @@ describe("applyExtraParamsToAgent", () => {
expect(payload.store).toBe(true);
});
it("strips disabled OpenAI reasoning payloads instead of sending effort:none", () => {
it("keeps disabled OpenAI reasoning payloads on native Responses routes", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai",
applyModelId: "gpt-5-mini",
@@ -2180,10 +2182,10 @@ describe("applyExtraParamsToAgent", () => {
reasoning: { effort: "none" },
},
});
expect(payload).not.toHaveProperty("reasoning");
expect(payload.reasoning).toEqual({ effort: "none" });
});
it("strips disabled Azure OpenAI Responses reasoning payloads", () => {
it("keeps disabled Azure OpenAI Responses reasoning payloads", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "azure-openai-responses",
applyModelId: "gpt-5-mini",
@@ -2198,7 +2200,7 @@ describe("applyExtraParamsToAgent", () => {
reasoning: { effort: "none" },
},
});
expect(payload).not.toHaveProperty("reasoning");
expect(payload.reasoning).toEqual({ effort: "none" });
});
it("injects configured OpenAI service_tier into Responses payloads", () => {

View File

@@ -359,7 +359,7 @@ export function createOpenAIDefaultTransportWrapper(baseStreamFn: StreamFn | und
const mergedOptions = {
...options,
transport: options?.transport ?? "auto",
openaiWsWarmup: typedOptions?.openaiWsWarmup ?? false,
openaiWsWarmup: typedOptions?.openaiWsWarmup ?? true,
} as SimpleStreamOptions;
return underlying(model, context, mergedOptions);
};

View File

@@ -268,7 +268,7 @@ describe("modelsListCommand forward-compat", () => {
id: "gpt-5.4",
name: "GPT-5.3 Codex",
input: ["text"],
contextWindow: 272000,
contextWindow: 400000,
},
]);
mocks.listProfilesForProvider.mockImplementation((_: unknown, provider: string) =>