fix(agents): preserve native runtime controls on Codex routes (#107588)

* fix(agents): preserve Codex runtime controls

* fix(agents): cover shipped fast cutoff aliases

* fix(agents): validate native runtime control values

* refactor(agents): simplify fast mode key matching

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Vito Cappello
2026-07-26 21:25:44 -04:00
committed by GitHub
parent b6dcb8c323
commit 5e841bc2e1
3 changed files with 126 additions and 3 deletions
+29
View File
@@ -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<AgentHarness["supports"]>[0]) =>
ctx.modelProvider?.requestTransportOverrides === "present"
+41 -3
View File
@@ -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<string, unknown>;
};
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<typeof resolveModelExtraParamSources>[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),
);
}
+56
View File
@@ -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: {} }),