diff --git a/src/agents/harness/selection.test.ts b/src/agents/harness/selection.test.ts index a2b7abb38c71..1108c27c1ccb 100644 --- a/src/agents/harness/selection.test.ts +++ b/src/agents/harness/selection.test.ts @@ -1503,6 +1503,35 @@ describe("selectAgentHarness", () => { }, ); + it("keeps native model run controls compatible with Codex", () => { + expect( + buildAgentHarnessSupportContext({ + provider: "openai", + modelId: "gpt-5.6-sol", + modelProvider: { + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + requestTransportOverrides: "none", + }, + requestedRuntime: "codex", + config: { + agents: { + defaults: { + models: { + "openai/gpt-5.6-sol": { + params: { thinking: "xhigh", fastMode: true, fastAutoOnSeconds: 30 }, + }, + }, + }, + }, + } as OpenClawConfig, + }).modelProvider, + ).toMatchObject({ + requestTransportOverrides: "none", + runtimePolicy: { compatibleIds: ["openclaw", "codex"] }, + }); + }); + it("rejects explicit Codex when agent request params cannot be reproduced", () => { const supports = vi.fn((ctx: Parameters[0]) => ctx.modelProvider?.requestTransportOverrides === "present" diff --git a/src/agents/model-extra-params.ts b/src/agents/model-extra-params.ts index ab1ce192c9d5..20cf6e060348 100644 --- a/src/agents/model-extra-params.ts +++ b/src/agents/model-extra-params.ts @@ -1,3 +1,5 @@ +import { normalizeFastMode } from "@openclaw/normalization-core/string-coerce"; +import { normalizeThinkLevel } from "../auto-reply/thinking.shared.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { modelKey } from "../shared/model-key.js"; import { resolveAgentConfig } from "./agent-scope-config.js"; @@ -8,6 +10,35 @@ type ModelExtraParamSources = { agentParams?: Record; }; +const FAST_MODE_CUTOFF_MODEL_PARAM_KEYS = new Set([ + "fastAutoOnSeconds", + "fastSeconds", + "fast_auto_on_seconds", + "fast_seconds", +]); + +// Native harnesses receive recognized values as typed run controls. Other value +// shapes with the same keys remain authored provider request parameters. +function isAgentRuntimeModelParam(key: string, value: unknown): boolean { + if (key === "thinking") { + return ( + value === false || + value === "disabled" || + value === "none" || + (typeof value === "string" && normalizeThinkLevel(value) !== undefined) + ); + } + if (key === "fastMode" || key === "fast_mode") { + return normalizeFastMode(value) !== undefined; + } + return ( + FAST_MODE_CUTOFF_MODEL_PARAM_KEYS.has(key) && + typeof value === "number" && + Number.isInteger(value) && + value > 0 + ); +} + function legacyModelKey(provider: string, modelId: string): string | undefined { const rawKey = `${provider.trim()}/${modelId.trim()}`; const canonicalKey = modelKey(provider, modelId); @@ -36,12 +67,19 @@ export function resolveModelExtraParamSources(params: { return { defaultParams, modelParams, agentParams }; } -/** Returns whether embedded OpenClaw would apply authored request parameters. */ +/** Returns whether embedded OpenClaw would apply authored provider request parameters. */ export function hasModelExtraParams( params: Parameters[0], ): boolean { const sources = resolveModelExtraParamSources(params); - return [sources.defaultParams, sources.modelParams, sources.agentParams].some( - (source) => source !== undefined && Object.keys(source).length > 0, + if ( + [sources.defaultParams, sources.agentParams].some( + (source) => source !== undefined && Object.keys(source).length > 0, + ) + ) { + return true; + } + return Object.entries(sources.modelParams ?? {}).some( + ([key, value]) => !isAgentRuntimeModelParam(key, value), ); } diff --git a/src/agents/openai-routing.test.ts b/src/agents/openai-routing.test.ts index eb4e44ae78bf..b3c4c69d914a 100644 --- a/src/agents/openai-routing.test.ts +++ b/src/agents/openai-routing.test.ts @@ -36,6 +36,62 @@ describe("OpenAI runtime routing policy", () => { ).toBe(true); }); + it.each([ + ["thinking", { thinking: "xhigh" }], + ["fastMode", { fastMode: true }], + ["fast_mode", { fast_mode: true }], + ["fastAutoOnSeconds", { fastMode: "auto", fastAutoOnSeconds: 30 }], + ["fast_auto_on_seconds", { fastMode: "auto", fast_auto_on_seconds: 30 }], + ["fastSeconds", { fastMode: "auto", fastSeconds: 30 }], + ["fast_seconds", { fastMode: "auto", fast_seconds: 30 }], + ])("keeps Codex for model-scoped %s controls", (_label, params) => { + const config = { + agents: { + defaults: { + models: { + "openai/gpt-5.6-sol": { + params, + }, + }, + }, + }, + } as OpenClawConfig; + + expect( + resolveOpenAIImplicitAgentRuntime({ + provider: "openai", + modelId: "gpt-5.6-sol", + config, + env: {}, + }), + ).toBe("codex"); + }); + + it.each([ + ["provider-native thinking", { thinking: { type: "enabled", budget_tokens: 2_048 } }], + ["invalid fast mode", { fastMode: { enabled: true } }], + ["invalid fast cutoff", { fastAutoOnSeconds: "30" }], + ])("keeps %s values on the OpenClaw runtime", (_label, params) => { + const config = { + agents: { + defaults: { + models: { + "openai/gpt-5.6-sol": { params }, + }, + }, + }, + } as OpenClawConfig; + + expect( + resolveOpenAIImplicitAgentRuntime({ + provider: "openai", + modelId: "gpt-5.6-sol", + config, + env: {}, + }), + ).toBe("openclaw"); + }); + it("maps provider route facts onto a closed implicit runtime", () => { expect( resolveOpenAIImplicitAgentRuntime({ provider: "openai", modelId: "gpt-5.6", env: {} }),