Skip to content

Commit 4eaf34e

Browse files
committed
Improve stale Codex auth recovery guidance
1 parent ff871e1 commit 4eaf34e

4 files changed

Lines changed: 152 additions & 4 deletions

File tree

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4169,6 +4169,44 @@ describe("runAgentTurnWithFallback", () => {
41694169
}
41704170
});
41714171

4172+
it("points stale openai-codex missing-key failures at doctor repair with re-auth fallback", async () => {
4173+
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
4174+
new Error('No API key found for provider "openai-codex".'),
4175+
);
4176+
4177+
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
4178+
const result = await runAgentTurnWithFallback({
4179+
commandBody: "hello",
4180+
followupRun: createFollowupRun(),
4181+
sessionCtx: {
4182+
Provider: "whatsapp",
4183+
MessageSid: "msg",
4184+
} as unknown as TemplateContext,
4185+
opts: {},
4186+
typingSignals: createMockTypingSignaler(),
4187+
blockReplyPipeline: null,
4188+
blockStreamingEnabled: false,
4189+
resolvedBlockStreamingBreak: "message_end",
4190+
applyReplyToMode: (payload) => payload,
4191+
shouldEmitToolResult: () => true,
4192+
shouldEmitToolOutput: () => false,
4193+
pendingToolTasks: new Set(),
4194+
resetSessionAfterCompactionFailure: async () => false,
4195+
resetSessionAfterRoleOrderingConflict: async () => false,
4196+
isHeartbeat: false,
4197+
sessionKey: "main",
4198+
getActiveSessionEntry: () => undefined,
4199+
resolvedVerboseLevel: "off",
4200+
});
4201+
4202+
expect(result.kind).toBe("final");
4203+
if (result.kind === "final") {
4204+
expect(result.payload.text).toBe(
4205+
"⚠️ 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`.",
4206+
);
4207+
}
4208+
});
4209+
41724210
it("falls back to a generic provider message for unsafe missing-key provider ids", async () => {
41734211
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
41744212
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
@@ -544,6 +544,9 @@ function buildMissingApiKeyFailureText(message: string): string | null {
544544
if (provider === "openai" && normalizedMessage.includes("OpenAI Codex OAuth")) {
545545
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.";
546546
}
547+
if (provider === "openai-codex") {
548+
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`.";
549+
}
547550
if (SAFE_MISSING_API_KEY_PROVIDERS.has(provider)) {
548551
return `⚠️ Missing API key for provider "${provider}". Configure the gateway auth for that provider, then try again.`;
549552
}

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,79 @@ describe("createModelSelectionState auto-failover overrides", () => {
949949
expect(state.resetModelOverride).toBe(false);
950950
});
951951

952+
it("clears stale auto-created legacy openai-codex route pins when primary is canonical openai", async () => {
953+
const sessionEntry = makeEntry({
954+
providerOverride: "openai-codex",
955+
modelOverride: "gpt-5.5",
956+
modelOverrideSource: "auto",
957+
modelProvider: "openai-codex",
958+
model: "gpt-5.5",
959+
contextTokens: 350_000,
960+
authProfileOverride: "openai-codex:default",
961+
authProfileOverrideSource: "auto",
962+
});
963+
const sessionStore = { [sessionKey]: sessionEntry };
964+
965+
const state = await createModelSelectionState({
966+
cfg: {} as OpenClawConfig,
967+
agentCfg: undefined,
968+
sessionEntry,
969+
sessionStore,
970+
sessionKey,
971+
defaultProvider: "openai",
972+
defaultModel: "gpt-5.5",
973+
primaryProvider: "openai",
974+
primaryModel: "gpt-5.5",
975+
provider: "openai-codex",
976+
model: "gpt-5.5",
977+
hasModelDirective: false,
978+
});
979+
980+
expect(state.provider).toBe("openai");
981+
expect(state.model).toBe("gpt-5.5");
982+
expect(state.resetModelOverride).toBe(true);
983+
expect(state.resetModelOverrideRef).toBe("openai-codex/gpt-5.5");
984+
expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined();
985+
expect(sessionStore[sessionKey]?.modelOverride).toBeUndefined();
986+
expect(sessionStore[sessionKey]?.modelOverrideSource).toBeUndefined();
987+
expect(sessionStore[sessionKey]?.modelProvider).toBeUndefined();
988+
expect(sessionStore[sessionKey]?.model).toBeUndefined();
989+
expect(sessionStore[sessionKey]?.contextTokens).toBeUndefined();
990+
expect(sessionStore[sessionKey]?.authProfileOverride).toBeUndefined();
991+
expect(sessionStore[sessionKey]?.authProfileOverrideSource).toBeUndefined();
992+
});
993+
994+
it("keeps explicit user openai-codex route overrides", async () => {
995+
const sessionEntry = makeEntry({
996+
providerOverride: "openai-codex",
997+
modelOverride: "gpt-5.5",
998+
modelOverrideSource: "user",
999+
});
1000+
const sessionStore = { [sessionKey]: sessionEntry };
1001+
1002+
const state = await createModelSelectionState({
1003+
cfg: {} as OpenClawConfig,
1004+
agentCfg: undefined,
1005+
sessionEntry,
1006+
sessionStore,
1007+
sessionKey,
1008+
defaultProvider: "openai",
1009+
defaultModel: "gpt-5.5",
1010+
primaryProvider: "openai",
1011+
primaryModel: "gpt-5.5",
1012+
provider: "openai",
1013+
model: "gpt-5.5",
1014+
hasModelDirective: false,
1015+
});
1016+
1017+
expect(state.provider).toBe("openai-codex");
1018+
expect(state.model).toBe("gpt-5.5");
1019+
expect(state.resetModelOverride).toBe(false);
1020+
expect(sessionStore[sessionKey]?.providerOverride).toBe("openai-codex");
1021+
expect(sessionStore[sessionKey]?.modelOverride).toBe("gpt-5.5");
1022+
expect(sessionStore[sessionKey]?.modelOverrideSource).toBe("user");
1023+
});
1024+
9521025
it("still clears disallowed auto-failover overrides through allowlist validation", async () => {
9531026
const cfg = {
9541027
agents: {

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import {
1818
createModelVisibilityPolicy,
1919
type ModelVisibilityPolicy,
2020
} from "../../agents/model-visibility-policy.js";
21-
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-codex-routing.js";
21+
import {
22+
OPENAI_CODEX_PROVIDER_ID,
23+
OPENAI_PROVIDER_ID,
24+
listOpenAIAuthProfileProvidersForAgentRuntime,
25+
} from "../../agents/openai-codex-routing.js";
2226
import type { SessionEntry } from "../../config/sessions/types.js";
2327
import type { OpenClawConfig } from "../../config/types.openclaw.js";
2428
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
@@ -182,6 +186,13 @@ export async function createModelSelectionState(params: {
182186
primaryProvider: params.primaryProvider,
183187
primaryModel: params.primaryModel,
184188
});
189+
const staleLegacyOpenAICodexAutoOverride =
190+
directStoredModelOverride?.source === "session" &&
191+
sessionEntry?.modelOverrideSource === "auto" &&
192+
normalizeProviderId(directStoredModelOverride.provider ?? "") === OPENAI_CODEX_PROVIDER_ID &&
193+
normalizeProviderId(primaryProvider) === OPENAI_PROVIDER_ID &&
194+
normalizeModelRef(OPENAI_PROVIDER_ID, directStoredModelOverride.model).model ===
195+
normalizeModelRef(OPENAI_PROVIDER_ID, primaryModel).model;
185196

186197
if (needsModelCatalog) {
187198
modelCatalog = await (await loadModelCatalogRuntime()).loadModelCatalog({ config: cfg });
@@ -229,11 +240,16 @@ export async function createModelSelectionState(params: {
229240
directStoredOverride.model,
230241
);
231242
const key = modelKey(normalizedOverride.provider, normalizedOverride.model);
232-
if (staleHeartbeatAutoFallbackOverride || !visibilityPolicy.allowsKey(key)) {
243+
if (
244+
staleHeartbeatAutoFallbackOverride ||
245+
staleLegacyOpenAICodexAutoOverride ||
246+
!visibilityPolicy.allowsKey(key)
247+
) {
233248
const { updated } = applyModelOverrideToSessionEntry({
234249
entry: sessionEntry,
235250
selection: { provider: primaryProvider, model: primaryModel, isDefault: true },
236-
preserveAuthProfileOverride: staleHeartbeatAutoFallbackOverride,
251+
preserveAuthProfileOverride:
252+
staleHeartbeatAutoFallbackOverride || staleLegacyOpenAICodexAutoOverride,
237253
});
238254
if (updated) {
239255
sessionStore[sessionKey] = sessionEntry;
@@ -268,6 +284,23 @@ export async function createModelSelectionState(params: {
268284
model = primaryModel;
269285
}
270286
}
287+
if (staleLegacyOpenAICodexAutoOverride) {
288+
const normalizedCurrentSelection = normalizeModelRef(provider, model);
289+
const currentSelectionKey = modelKey(
290+
normalizedCurrentSelection.provider,
291+
normalizedCurrentSelection.model,
292+
);
293+
const normalizedDirectOverride = directStoredOverride
294+
? normalizeModelRef(directStoredOverride.provider, directStoredOverride.model)
295+
: null;
296+
const directStoredOverrideKey = normalizedDirectOverride
297+
? modelKey(normalizedDirectOverride.provider, normalizedDirectOverride.model)
298+
: undefined;
299+
if (currentSelectionKey === directStoredOverrideKey) {
300+
provider = primaryProvider;
301+
model = primaryModel;
302+
}
303+
}
271304

272305
const storedOverride = resolveStoredModelOverride({
273306
sessionEntry,
@@ -284,7 +317,8 @@ export async function createModelSelectionState(params: {
284317
params.skipStoredModelOverride === true ||
285318
hasOneTurnModelOverride ||
286319
params.hasResolvedHeartbeatModelOverride === true ||
287-
(staleHeartbeatAutoFallbackOverride && storedOverride?.source === "session");
320+
(staleHeartbeatAutoFallbackOverride && storedOverride?.source === "session") ||
321+
(staleLegacyOpenAICodexAutoOverride && storedOverride?.source === "session");
288322

289323
if (storedOverride?.model && !skipStoredOverride) {
290324
const normalizedStoredOverride = normalizeModelRef(

0 commit comments

Comments
 (0)