fix(agents): allow Codex runtime for Codex provider (#103775)

This commit is contained in:
Peter Steinberger
2026-07-10 16:49:15 +01:00
committed by GitHub
parent d628e35574
commit db39fe8072
5 changed files with 66 additions and 29 deletions
+1
View File
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- **Codex runtime switching:** accept the bundled Codex runtime for both `codex/*` and `openai/*` model routes while keeping unsupported provider/runtime pairs rejected. (#103762)
- **Agent abort cleanup:** serialize prompt lock reacquisition with terminal cleanup so canceled embedded runs do not self-contend on session locks for up to 60 seconds.
- **Chutes OAuth deadlines:** bound token exchange, profile lookup, and refresh requests, and keep issued tokens when optional userinfo enrichment stalls. (#102026) Thanks @Alix-007.
- **Control UI workspace avatars:** inline validated agent avatar files in bootstrap and identity responses so Personal card images render without unauthenticated avatar-route requests, while preserving configured emoji precedence. (#102892, #97602) Thanks @LZY3538.
+26 -12
View File
@@ -24,6 +24,27 @@ export function resolvePersistedSessionRuntimeId(
return normalizeOptionalAgentRuntimeId(entry?.agentHarnessId);
}
/** Resolves a runtime id only when it can serve the selected provider. */
export function resolveCompatibleAgentRuntimeForProvider(params: {
provider?: string | null;
runtime?: string | null;
cfg?: OpenClawConfig;
}): string | undefined {
const runtime = normalizeOptionalAgentRuntimeId(params.runtime);
if (!runtime || isDefaultAgentRuntimeId(runtime)) {
return undefined;
}
if (runtime === "openclaw") {
return runtime;
}
const provider = params.provider?.trim().toLowerCase() ?? "";
// The Codex harness owns both OpenClaw's virtual Codex namespace and canonical OpenAI routes.
if (runtime === "codex" && (provider === "codex" || provider === "openai")) {
return runtime;
}
return isCliRuntimeAliasForProvider({ provider, runtime, cfg: params.cfg }) ? runtime : undefined;
}
/** Resolves a persisted runtime override only when it can serve the selected provider. */
export function resolveSessionRuntimeOverrideForProvider(params: {
provider?: string | null;
@@ -32,16 +53,9 @@ export function resolveSessionRuntimeOverrideForProvider(params: {
}): string | undefined {
// agentHarnessId records the runtime that produced the existing transcript;
// it must not override the runtime selected for the next turn.
const runtime = normalizeOptionalAgentRuntimeId(params.entry?.agentRuntimeOverride);
if (!runtime || isDefaultAgentRuntimeId(runtime)) {
return undefined;
}
if (runtime === "openclaw") {
return runtime;
}
const provider = params.provider?.trim().toLowerCase() ?? "";
if (provider === "openai" && runtime === "codex") {
return runtime;
}
return isCliRuntimeAliasForProvider({ provider, runtime, cfg: params.cfg }) ? runtime : undefined;
return resolveCompatibleAgentRuntimeForProvider({
provider: params.provider,
runtime: params.entry?.agentRuntimeOverride,
cfg: params.cfg,
});
}
@@ -86,6 +86,19 @@ describe("resolveSessionRuntimeOverrideForProvider", () => {
).toBeUndefined();
});
it.each([
{ provider: "openai", expected: "codex" },
{ provider: "codex", expected: "codex" },
{ provider: "anthropic", expected: undefined },
])("resolves Codex runtime compatibility for $provider", ({ provider, expected }) => {
expect(
resolveSessionRuntimeOverrideForProvider({
provider,
entry: { agentRuntimeOverride: "codex" },
}),
).toBe(expected);
});
it("keeps CLI runtime pins only when the runtime serves the selected provider", () => {
cliBackendsTesting.setDepsForTest({
resolveRuntimeCliBackends: () => [],
@@ -3,9 +3,11 @@ import {
isDefaultAgentRuntimeId,
normalizeOptionalAgentRuntimeId,
} from "../../agents/agent-runtime-id.js";
import { resolveCliRuntimeModelBackendBinding } from "../../agents/cli-backends.js";
import { normalizeProviderId } from "../../agents/model-selection.js";
import { resolveSessionRuntimeOverrideForProvider } from "../../agents/session-runtime-compat.js";
import {
resolveCompatibleAgentRuntimeForProvider,
resolveSessionRuntimeOverrideForProvider,
} from "../../agents/session-runtime-compat.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
@@ -42,21 +44,15 @@ export function resolveModelRuntimeDirective(params: {
if (isDefaultAgentRuntimeId(runtime)) {
return { kind: "clear" };
}
if (runtime === "openclaw") {
return { kind: "set", runtime };
}
const provider = normalizeProviderId(params.provider);
if (provider === "openai" && runtime === "codex") {
return { kind: "set", runtime };
}
const backend = resolveCliRuntimeModelBackendBinding({
config: params.cfg,
const compatibleRuntime = resolveCompatibleAgentRuntimeForProvider({
provider,
runtime,
cfg: params.cfg,
});
if (backend) {
return { kind: "set", runtime: backend.runtime };
if (compatibleRuntime) {
return { kind: "set", runtime: compatibleRuntime };
}
return {
@@ -1306,14 +1306,17 @@ describe("/model chat UX", () => {
expect(sessionEntry.authProfileOverride).toBe(OPENAI_DATE_PROFILE_ID);
});
it("persists provider-compatible runtime overrides for mixed-content messages", async () => {
it.each([
["openai/gpt-4o", "openai", "gpt-4o"],
["codex/gpt-5.5", "codex", "gpt-5.5"],
])("persists provider-compatible runtime overrides for %s", async (modelKey, provider, model) => {
const { persisted, sessionEntry } = await persistModelDirectiveForTest({
command: "/model openai/gpt-4o --runtime codex hello",
allowedModelKeys: ["openai/gpt-4o"],
command: `/model ${modelKey} --runtime codex hello`,
allowedModelKeys: [modelKey],
});
expect(sessionEntry.providerOverride).toBe("openai");
expect(sessionEntry.modelOverride).toBe("gpt-4o");
expect(sessionEntry.providerOverride).toBe(provider);
expect(sessionEntry.modelOverride).toBe(model);
expect(sessionEntry.agentRuntimeOverride).toBe("codex");
expect(persisted.runtimeChange).toEqual({ kind: "set", runtime: "codex" });
});
@@ -1443,6 +1446,16 @@ describe("/model chat UX", () => {
expect(enqueueSystemEvent).not.toHaveBeenCalled();
});
it("rejects the Codex runtime for providers the harness does not support", async () => {
const { persisted, sessionEntry } = await persistModelDirectiveForTest({
command: "/model anthropic/claude-opus-4-6 --runtime codex hello",
allowedModelKeys: ["anthropic/claude-opus-4-6"],
});
expect(persisted.errorText).toBe('Runtime "codex" is not supported for anthropic.');
expect(sessionEntry.agentRuntimeOverride).toBeUndefined();
});
it("rejects unsupported mixed thinking before mutating the model/runtime transaction", async () => {
setOpenAiRuntimeScopedUltraProvider();
const sessionEntry = createSessionEntry({