mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 15:11:42 +00:00
fix: align native openai transport defaults
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user