mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 05:33:39 +00:00
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>
This commit is contained in:
@@ -5270,6 +5270,22 @@ describe("runAgentTurnWithFallback", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("points stale openai-codex missing-key failures at doctor repair with re-auth fallback", async () => {
|
||||
state.runEmbeddedAgentMock.mockRejectedValueOnce(
|
||||
new Error('No API key found for provider "openai-codex".'),
|
||||
);
|
||||
|
||||
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
|
||||
const result = await runAgentTurnWithFallback(createMinimalRunAgentTurnParams());
|
||||
|
||||
expect(result.kind).toBe("final");
|
||||
if (result.kind === "final") {
|
||||
expect(result.payload.text).toBe(
|
||||
"⚠️ 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`.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to a generic provider message for unsafe missing-key provider ids", async () => {
|
||||
state.runEmbeddedAgentMock.mockRejectedValueOnce(
|
||||
new Error('No API key found for provider "openai`\nrm -rf /".'),
|
||||
|
||||
@@ -716,6 +716,9 @@ function buildMissingApiKeyFailureText(message: string): string | null {
|
||||
if (provider === "openai" && normalizedMessage.includes("OpenAI Codex OAuth")) {
|
||||
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.";
|
||||
}
|
||||
if (provider === "openai-codex") {
|
||||
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`.";
|
||||
}
|
||||
if (SAFE_MISSING_API_KEY_PROVIDERS.has(provider)) {
|
||||
return `⚠️ Missing API key for provider "${provider}". Configure the gateway auth for that provider, then try again.`;
|
||||
}
|
||||
|
||||
@@ -1058,6 +1058,164 @@ describe("createModelSelectionState auto-failover overrides", () => {
|
||||
expect(state.resetModelOverride).toBe(false);
|
||||
});
|
||||
|
||||
it("clears stale auto-created legacy openai-codex route pins when primary is canonical openai", async () => {
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openai-codex",
|
||||
modelOverride: "gpt-5.5",
|
||||
modelOverrideSource: "auto",
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.5",
|
||||
contextTokens: 350_000,
|
||||
authProfileOverride: "openai-codex:default",
|
||||
authProfileOverrideSource: "auto",
|
||||
});
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg: {} as OpenClawConfig,
|
||||
agentCfg: undefined,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-5.5",
|
||||
primaryProvider: "openai",
|
||||
primaryModel: "gpt-5.5",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.5",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
expect(state.provider).toBe("openai");
|
||||
expect(state.model).toBe("gpt-5.5");
|
||||
expect(state.resetModelOverride).toBe(true);
|
||||
expect(state.resetModelOverrideRef).toBe("openai-codex/gpt-5.5");
|
||||
expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.modelOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.modelOverrideSource).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.modelProvider).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.model).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.contextTokens).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.authProfileOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.authProfileOverrideSource).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves usable Codex auth while clearing stale legacy openai-codex route pins", async () => {
|
||||
authProfileStoreMock.store = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:default": {
|
||||
type: "api_key",
|
||||
provider: "openai-codex",
|
||||
key: "test-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openai-codex",
|
||||
modelOverride: "gpt-5.5",
|
||||
modelOverrideSource: "auto",
|
||||
authProfileOverride: "openai-codex:default",
|
||||
authProfileOverrideSource: "auto",
|
||||
});
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg: {} as OpenClawConfig,
|
||||
agentCfg: undefined,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-5.5",
|
||||
primaryProvider: "openai",
|
||||
primaryModel: "gpt-5.5",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.5",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
expect(state.provider).toBe("openai");
|
||||
expect(state.model).toBe("gpt-5.5");
|
||||
expect(state.resetModelOverride).toBe(true);
|
||||
expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.modelOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.authProfileOverride).toBe("openai-codex:default");
|
||||
expect(sessionStore[sessionKey]?.authProfileOverrideSource).toBe("auto");
|
||||
});
|
||||
|
||||
it("keeps auto openai-codex pins when canonical openai uses a custom API route", async () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://proxy.example.test/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openai-codex",
|
||||
modelOverride: "gpt-5.5",
|
||||
modelOverrideSource: "auto",
|
||||
});
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentCfg: undefined,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-5.5",
|
||||
primaryProvider: "openai",
|
||||
primaryModel: "gpt-5.5",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.5",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
expect(state.provider).toBe("openai-codex");
|
||||
expect(state.model).toBe("gpt-5.5");
|
||||
expect(state.resetModelOverride).toBe(false);
|
||||
expect(sessionStore[sessionKey]?.providerOverride).toBe("openai-codex");
|
||||
expect(sessionStore[sessionKey]?.modelOverride).toBe("gpt-5.5");
|
||||
expect(sessionStore[sessionKey]?.modelOverrideSource).toBe("auto");
|
||||
});
|
||||
|
||||
it("keeps explicit user openai-codex route overrides", async () => {
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openai-codex",
|
||||
modelOverride: "gpt-5.5",
|
||||
modelOverrideSource: "user",
|
||||
});
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg: {} as OpenClawConfig,
|
||||
agentCfg: undefined,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-5.5",
|
||||
primaryProvider: "openai",
|
||||
primaryModel: "gpt-5.5",
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
expect(state.provider).toBe("openai-codex");
|
||||
expect(state.model).toBe("gpt-5.5");
|
||||
expect(state.resetModelOverride).toBe(false);
|
||||
expect(sessionStore[sessionKey]?.providerOverride).toBe("openai-codex");
|
||||
expect(sessionStore[sessionKey]?.modelOverride).toBe("gpt-5.5");
|
||||
expect(sessionStore[sessionKey]?.modelOverrideSource).toBe("user");
|
||||
});
|
||||
|
||||
it("still clears disallowed auto-failover overrides through allowlist validation", async () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
createModelVisibilityPolicy,
|
||||
type ModelVisibilityPolicy,
|
||||
} from "../../agents/model-visibility-policy.js";
|
||||
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-codex-routing.js";
|
||||
import {
|
||||
OPENAI_CODEX_PROVIDER_ID,
|
||||
OPENAI_PROVIDER_ID,
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime,
|
||||
} from "../../agents/openai-codex-routing.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
@@ -195,6 +199,23 @@ export async function createModelSelectionState(params: {
|
||||
primaryProvider: params.primaryProvider,
|
||||
primaryModel: params.primaryModel,
|
||||
});
|
||||
const primaryHarnessPolicy = resolveAgentHarnessPolicy({
|
||||
provider: primaryProvider,
|
||||
modelId: primaryModel,
|
||||
config: cfg,
|
||||
agentId: params.agentId,
|
||||
sessionKey,
|
||||
});
|
||||
const staleLegacyOpenAICodexAutoOverride =
|
||||
directStoredModelOverride?.source === "session" &&
|
||||
sessionEntry?.modelOverrideSource === "auto" &&
|
||||
normalizeProviderId(directStoredModelOverride.provider ?? "") === OPENAI_CODEX_PROVIDER_ID &&
|
||||
normalizeProviderId(primaryProvider) === OPENAI_PROVIDER_ID &&
|
||||
primaryHarnessPolicy.runtime === "codex" &&
|
||||
normalizeRuntimeModelRef(OPENAI_PROVIDER_ID, directStoredModelOverride.model).model ===
|
||||
normalizeRuntimeModelRef(OPENAI_PROVIDER_ID, primaryModel).model;
|
||||
const staleDirectStoredOverride =
|
||||
staleHeartbeatAutoFallbackOverride || staleLegacyOpenAICodexAutoOverride;
|
||||
|
||||
if (needsModelCatalog) {
|
||||
modelCatalog = await (await loadModelCatalogRuntime()).loadModelCatalog({ config: cfg });
|
||||
@@ -238,11 +259,11 @@ export async function createModelSelectionState(params: {
|
||||
directStoredOverride.model,
|
||||
);
|
||||
const key = modelKey(normalizedOverride.provider, normalizedOverride.model);
|
||||
if (staleHeartbeatAutoFallbackOverride || !visibilityPolicy.allowsKey(key)) {
|
||||
if (staleDirectStoredOverride || !visibilityPolicy.allowsKey(key)) {
|
||||
const { updated } = applyModelOverrideToSessionEntry({
|
||||
entry: sessionEntry,
|
||||
selection: { provider: primaryProvider, model: primaryModel, isDefault: true },
|
||||
preserveAuthProfileOverride: staleHeartbeatAutoFallbackOverride,
|
||||
preserveAuthProfileOverride: staleDirectStoredOverride,
|
||||
});
|
||||
if (updated) {
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
@@ -260,7 +281,7 @@ export async function createModelSelectionState(params: {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (staleHeartbeatAutoFallbackOverride) {
|
||||
if (staleDirectStoredOverride) {
|
||||
const normalizedCurrentSelection = normalizeRuntimeModelRef(provider, model);
|
||||
const currentSelectionKey = modelKey(
|
||||
normalizedCurrentSelection.provider,
|
||||
@@ -292,7 +313,7 @@ export async function createModelSelectionState(params: {
|
||||
const skipStoredOverride =
|
||||
params.skipStoredModelOverride === true ||
|
||||
params.hasResolvedHeartbeatModelOverride === true ||
|
||||
(staleHeartbeatAutoFallbackOverride && storedOverride?.source === "session");
|
||||
(staleDirectStoredOverride && storedOverride?.source === "session");
|
||||
|
||||
if (storedOverride?.model && !skipStoredOverride) {
|
||||
const normalizedStoredOverride = normalizeRuntimeModelRef(
|
||||
|
||||
Reference in New Issue
Block a user