Skip to content

Commit 77fe36b

Browse files
authored
Improve stale Codex auth recovery guidance
Fixes #83935. Summary: - clear stale legacy openai-codex auto route pins only when the canonical OpenAI provider is still using the Codex harness for the same model - preserve usable Codex auth profiles while clearing stale route state - keep explicit/custom OpenAI API route pins intact Verification: - git diff --check - pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/model-selection.ts src/auto-reply/reply/model-selection.test.ts src/auto-reply/reply/agent-runner-execution.ts src/auto-reply/reply/agent-runner-execution.test.ts - fnm exec --using 24.15.0 node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo - .agents/skills/autoreview/scripts/autoreview --mode local - CI: https://github.com/openclaw/openclaw/actions/runs/26542490863 Co-authored-by: Paul Frederiksen <paul@paulfrederiksen.com>
1 parent 316fd5b commit 77fe36b

4 files changed

Lines changed: 203 additions & 5 deletions

File tree

src/auto-reply/reply/agent-runner-execution.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5270,6 +5270,22 @@ describe("runAgentTurnWithFallback", () => {
52705270
}
52715271
});
52725272

5273+
it("points stale openai-codex missing-key failures at doctor repair with re-auth fallback", async () => {
5274+
state.runEmbeddedAgentMock.mockRejectedValueOnce(
5275+
new Error('No API key found for provider "openai-codex".'),
5276+
);
5277+
5278+
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
5279+
const result = await runAgentTurnWithFallback(createMinimalRunAgentTurnParams());
5280+
5281+
expect(result.kind).toBe("final");
5282+
if (result.kind === "final") {
5283+
expect(result.payload.text).toBe(
5284+
"⚠️ The session is pointing at a stale OpenAI Codex auth route. Run `openclaw doctor --fix` to repair Codex model/session routes, restart the gateway if doctor asks, then try again. If doctor has nothing to repair or the error persists, re-auth with `openclaw models auth login --provider openai-codex` or run `openclaw configure`.",
5285+
);
5286+
}
5287+
});
5288+
52735289
it("falls back to a generic provider message for unsafe missing-key provider ids", async () => {
52745290
state.runEmbeddedAgentMock.mockRejectedValueOnce(
52755291
new Error('No API key found for provider "openai`\nrm -rf /".'),

src/auto-reply/reply/agent-runner-execution.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,9 @@ function buildMissingApiKeyFailureText(message: string): string | null {
716716
if (provider === "openai" && normalizedMessage.includes("OpenAI Codex OAuth")) {
717717
return "⚠️ Missing API key for OpenAI on the gateway. Use `openai/gpt-5.5` with the Codex OAuth profile, or set `OPENAI_API_KEY` for direct OpenAI API-key runs.";
718718
}
719+
if (provider === "openai-codex") {
720+
return "⚠️ The session is pointing at a stale OpenAI Codex auth route. Run `openclaw doctor --fix` to repair Codex model/session routes, restart the gateway if doctor asks, then try again. If doctor has nothing to repair or the error persists, re-auth with `openclaw models auth login --provider openai-codex` or run `openclaw configure`.";
721+
}
719722
if (SAFE_MISSING_API_KEY_PROVIDERS.has(provider)) {
720723
return `⚠️ Missing API key for provider "${provider}". Configure the gateway auth for that provider, then try again.`;
721724
}

src/auto-reply/reply/model-selection.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,164 @@ describe("createModelSelectionState auto-failover overrides", () => {
10581058
expect(state.resetModelOverride).toBe(false);
10591059
});
10601060

1061+
it("clears stale auto-created legacy openai-codex route pins when primary is canonical openai", async () => {
1062+
const sessionEntry = makeEntry({
1063+
providerOverride: "openai-codex",
1064+
modelOverride: "gpt-5.5",
1065+
modelOverrideSource: "auto",
1066+
modelProvider: "openai-codex",
1067+
model: "gpt-5.5",
1068+
contextTokens: 350_000,
1069+
authProfileOverride: "openai-codex:default",
1070+
authProfileOverrideSource: "auto",
1071+
});
1072+
const sessionStore = { [sessionKey]: sessionEntry };
1073+
1074+
const state = await createModelSelectionState({
1075+
cfg: {} as OpenClawConfig,
1076+
agentCfg: undefined,
1077+
sessionEntry,
1078+
sessionStore,
1079+
sessionKey,
1080+
defaultProvider: "openai",
1081+
defaultModel: "gpt-5.5",
1082+
primaryProvider: "openai",
1083+
primaryModel: "gpt-5.5",
1084+
provider: "openai-codex",
1085+
model: "gpt-5.5",
1086+
hasModelDirective: false,
1087+
});
1088+
1089+
expect(state.provider).toBe("openai");
1090+
expect(state.model).toBe("gpt-5.5");
1091+
expect(state.resetModelOverride).toBe(true);
1092+
expect(state.resetModelOverrideRef).toBe("openai-codex/gpt-5.5");
1093+
expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined();
1094+
expect(sessionStore[sessionKey]?.modelOverride).toBeUndefined();
1095+
expect(sessionStore[sessionKey]?.modelOverrideSource).toBeUndefined();
1096+
expect(sessionStore[sessionKey]?.modelProvider).toBeUndefined();
1097+
expect(sessionStore[sessionKey]?.model).toBeUndefined();
1098+
expect(sessionStore[sessionKey]?.contextTokens).toBeUndefined();
1099+
expect(sessionStore[sessionKey]?.authProfileOverride).toBeUndefined();
1100+
expect(sessionStore[sessionKey]?.authProfileOverrideSource).toBeUndefined();
1101+
});
1102+
1103+
it("preserves usable Codex auth while clearing stale legacy openai-codex route pins", async () => {
1104+
authProfileStoreMock.store = {
1105+
version: 1,
1106+
profiles: {
1107+
"openai-codex:default": {
1108+
type: "api_key",
1109+
provider: "openai-codex",
1110+
key: "test-key",
1111+
},
1112+
},
1113+
};
1114+
const sessionEntry = makeEntry({
1115+
providerOverride: "openai-codex",
1116+
modelOverride: "gpt-5.5",
1117+
modelOverrideSource: "auto",
1118+
authProfileOverride: "openai-codex:default",
1119+
authProfileOverrideSource: "auto",
1120+
});
1121+
const sessionStore = { [sessionKey]: sessionEntry };
1122+
1123+
const state = await createModelSelectionState({
1124+
cfg: {} as OpenClawConfig,
1125+
agentCfg: undefined,
1126+
sessionEntry,
1127+
sessionStore,
1128+
sessionKey,
1129+
defaultProvider: "openai",
1130+
defaultModel: "gpt-5.5",
1131+
primaryProvider: "openai",
1132+
primaryModel: "gpt-5.5",
1133+
provider: "openai-codex",
1134+
model: "gpt-5.5",
1135+
hasModelDirective: false,
1136+
});
1137+
1138+
expect(state.provider).toBe("openai");
1139+
expect(state.model).toBe("gpt-5.5");
1140+
expect(state.resetModelOverride).toBe(true);
1141+
expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined();
1142+
expect(sessionStore[sessionKey]?.modelOverride).toBeUndefined();
1143+
expect(sessionStore[sessionKey]?.authProfileOverride).toBe("openai-codex:default");
1144+
expect(sessionStore[sessionKey]?.authProfileOverrideSource).toBe("auto");
1145+
});
1146+
1147+
it("keeps auto openai-codex pins when canonical openai uses a custom API route", async () => {
1148+
const cfg = {
1149+
models: {
1150+
providers: {
1151+
openai: {
1152+
baseUrl: "https://proxy.example.test/v1",
1153+
models: [],
1154+
},
1155+
},
1156+
},
1157+
} as OpenClawConfig;
1158+
const sessionEntry = makeEntry({
1159+
providerOverride: "openai-codex",
1160+
modelOverride: "gpt-5.5",
1161+
modelOverrideSource: "auto",
1162+
});
1163+
const sessionStore = { [sessionKey]: sessionEntry };
1164+
1165+
const state = await createModelSelectionState({
1166+
cfg,
1167+
agentCfg: undefined,
1168+
sessionEntry,
1169+
sessionStore,
1170+
sessionKey,
1171+
defaultProvider: "openai",
1172+
defaultModel: "gpt-5.5",
1173+
primaryProvider: "openai",
1174+
primaryModel: "gpt-5.5",
1175+
provider: "openai-codex",
1176+
model: "gpt-5.5",
1177+
hasModelDirective: false,
1178+
});
1179+
1180+
expect(state.provider).toBe("openai-codex");
1181+
expect(state.model).toBe("gpt-5.5");
1182+
expect(state.resetModelOverride).toBe(false);
1183+
expect(sessionStore[sessionKey]?.providerOverride).toBe("openai-codex");
1184+
expect(sessionStore[sessionKey]?.modelOverride).toBe("gpt-5.5");
1185+
expect(sessionStore[sessionKey]?.modelOverrideSource).toBe("auto");
1186+
});
1187+
1188+
it("keeps explicit user openai-codex route overrides", async () => {
1189+
const sessionEntry = makeEntry({
1190+
providerOverride: "openai-codex",
1191+
modelOverride: "gpt-5.5",
1192+
modelOverrideSource: "user",
1193+
});
1194+
const sessionStore = { [sessionKey]: sessionEntry };
1195+
1196+
const state = await createModelSelectionState({
1197+
cfg: {} as OpenClawConfig,
1198+
agentCfg: undefined,
1199+
sessionEntry,
1200+
sessionStore,
1201+
sessionKey,
1202+
defaultProvider: "openai",
1203+
defaultModel: "gpt-5.5",
1204+
primaryProvider: "openai",
1205+
primaryModel: "gpt-5.5",
1206+
provider: "openai",
1207+
model: "gpt-5.5",
1208+
hasModelDirective: false,
1209+
});
1210+
1211+
expect(state.provider).toBe("openai-codex");
1212+
expect(state.model).toBe("gpt-5.5");
1213+
expect(state.resetModelOverride).toBe(false);
1214+
expect(sessionStore[sessionKey]?.providerOverride).toBe("openai-codex");
1215+
expect(sessionStore[sessionKey]?.modelOverride).toBe("gpt-5.5");
1216+
expect(sessionStore[sessionKey]?.modelOverrideSource).toBe("user");
1217+
});
1218+
10611219
it("still clears disallowed auto-failover overrides through allowlist validation", async () => {
10621220
const cfg = {
10631221
agents: {

src/auto-reply/reply/model-selection.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import {
1919
createModelVisibilityPolicy,
2020
type ModelVisibilityPolicy,
2121
} from "../../agents/model-visibility-policy.js";
22-
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-codex-routing.js";
22+
import {
23+
OPENAI_CODEX_PROVIDER_ID,
24+
OPENAI_PROVIDER_ID,
25+
listOpenAIAuthProfileProvidersForAgentRuntime,
26+
} from "../../agents/openai-codex-routing.js";
2327
import type { SessionEntry } from "../../config/sessions/types.js";
2428
import type { OpenClawConfig } from "../../config/types.openclaw.js";
2529
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
@@ -195,6 +199,23 @@ export async function createModelSelectionState(params: {
195199
primaryProvider: params.primaryProvider,
196200
primaryModel: params.primaryModel,
197201
});
202+
const primaryHarnessPolicy = resolveAgentHarnessPolicy({
203+
provider: primaryProvider,
204+
modelId: primaryModel,
205+
config: cfg,
206+
agentId: params.agentId,
207+
sessionKey,
208+
});
209+
const staleLegacyOpenAICodexAutoOverride =
210+
directStoredModelOverride?.source === "session" &&
211+
sessionEntry?.modelOverrideSource === "auto" &&
212+
normalizeProviderId(directStoredModelOverride.provider ?? "") === OPENAI_CODEX_PROVIDER_ID &&
213+
normalizeProviderId(primaryProvider) === OPENAI_PROVIDER_ID &&
214+
primaryHarnessPolicy.runtime === "codex" &&
215+
normalizeRuntimeModelRef(OPENAI_PROVIDER_ID, directStoredModelOverride.model).model ===
216+
normalizeRuntimeModelRef(OPENAI_PROVIDER_ID, primaryModel).model;
217+
const staleDirectStoredOverride =
218+
staleHeartbeatAutoFallbackOverride || staleLegacyOpenAICodexAutoOverride;
198219

199220
if (needsModelCatalog) {
200221
modelCatalog = await (await loadModelCatalogRuntime()).loadModelCatalog({ config: cfg });
@@ -238,11 +259,11 @@ export async function createModelSelectionState(params: {
238259
directStoredOverride.model,
239260
);
240261
const key = modelKey(normalizedOverride.provider, normalizedOverride.model);
241-
if (staleHeartbeatAutoFallbackOverride || !visibilityPolicy.allowsKey(key)) {
262+
if (staleDirectStoredOverride || !visibilityPolicy.allowsKey(key)) {
242263
const { updated } = applyModelOverrideToSessionEntry({
243264
entry: sessionEntry,
244265
selection: { provider: primaryProvider, model: primaryModel, isDefault: true },
245-
preserveAuthProfileOverride: staleHeartbeatAutoFallbackOverride,
266+
preserveAuthProfileOverride: staleDirectStoredOverride,
246267
});
247268
if (updated) {
248269
sessionStore[sessionKey] = sessionEntry;
@@ -260,7 +281,7 @@ export async function createModelSelectionState(params: {
260281
}
261282
}
262283
}
263-
if (staleHeartbeatAutoFallbackOverride) {
284+
if (staleDirectStoredOverride) {
264285
const normalizedCurrentSelection = normalizeRuntimeModelRef(provider, model);
265286
const currentSelectionKey = modelKey(
266287
normalizedCurrentSelection.provider,
@@ -292,7 +313,7 @@ export async function createModelSelectionState(params: {
292313
const skipStoredOverride =
293314
params.skipStoredModelOverride === true ||
294315
params.hasResolvedHeartbeatModelOverride === true ||
295-
(staleHeartbeatAutoFallbackOverride && storedOverride?.source === "session");
316+
(staleDirectStoredOverride && storedOverride?.source === "session");
296317

297318
if (storedOverride?.model && !skipStoredOverride) {
298319
const normalizedStoredOverride = normalizeRuntimeModelRef(

0 commit comments

Comments
 (0)