From 9dedc4d95cfb8b5fee3019a073d19ac65fc11367 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 15:26:27 +0100 Subject: [PATCH] fix: honor Codex auth order for OpenAI PI (#82605) * fix: honor Codex auth order for OpenAI PI * docs: add PR reference for OpenAI PI auth fix --- CHANGELOG.md | 1 + src/agents/agent-command.ts | 1 + src/agents/auth-profiles/order.test.ts | 34 +++++++ src/agents/btw.ts | 1 + src/agents/command/attempt-execution.ts | 8 +- src/agents/model-auth-label.ts | 36 ++++++-- src/agents/openai-codex-routing.test.ts | 91 +++++++++++++++++++ src/agents/openai-codex-routing.ts | 49 +++++++++- src/agents/pi-embedded-runner.e2e.test.ts | 63 +++++++++++++ src/agents/pi-embedded-runner/run.ts | 59 ++++++++++-- .../reply/agent-runner-execution.ts | 9 +- src/auto-reply/reply/commands-status.test.ts | 81 +++++++++++++++++ src/auto-reply/reply/get-reply-run.ts | 1 + src/auto-reply/reply/model-selection.ts | 1 + src/cli/models-cli.test.ts | 16 ++++ src/cli/models-cli.ts | 8 +- src/commands/auth-choice.model-check.ts | 1 + src/cron/isolated-agent/run.ts | 1 + src/status/status-text.ts | 15 ++- 19 files changed, 450 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f715191c74f..d62125ba90f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai - Telegram: persist polling updates through restart replay so queued same-topic messages resume in order instead of losing context after a gateway restart. (#82256) Thanks @VACInc. - Gateway/Gmail: abort in-flight Gmail watcher startup and hot-reload restarts before shutdown so reloads cannot spawn `gog serve` after the Gateway is closing. Thanks @frankekn. - Agents/Codex: fall back to the embedded PI runner when OpenAI's implicit Codex harness preference cannot find a registered Codex plugin, preventing OpenAI-compatible gateway requests from failing with an unregistered harness error. Fixes #82437. +- Agents/OpenAI: honor `openai-codex:*` entries placed ahead of API-key backups in `auth.order.openai` for explicit OpenAI PI runs, and accept `models auth login --provider openai-codex --device-code` for headless sign-in. Fixes #82521. (#82605) - CLI/channels: install missing externalized same-id channel plugins during `channels add --channel `, so recovery for WhatsApp and other externalized stock channels does not require a separate `plugins enable` step. Fixes #82533. - MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. Fixes #82424. (#82443) Thanks @joshavant. - Plugins: accept deprecated `api.on("deactivate")` registrations as a dated compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown while authors get migration guidance. diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index 83389c95f82..bc4d329c4f7 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -894,6 +894,7 @@ async function agentCommandInternal( const acceptedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({ provider: providerForAuthProfileValidation, harnessRuntime: validationHarnessPolicy.runtime, + config: cfg, }).map((candidateProvider) => resolveProviderIdForAuth(candidateProvider, { config: cfg, workspaceDir }), ); diff --git a/src/agents/auth-profiles/order.test.ts b/src/agents/auth-profiles/order.test.ts index 90e838982fb..17c99faac5d 100644 --- a/src/agents/auth-profiles/order.test.ts +++ b/src/agents/auth-profiles/order.test.ts @@ -337,6 +337,40 @@ describe("resolveAuthProfileOrder", () => { expect(order).toEqual(["openai-codex:personal", "openai:default"]); }); + it("keeps Codex profiles listed in the friendly OpenAI order for Codex auth", async () => { + const store: AuthProfileStore = { + version: 1, + profiles: { + "openai-codex:personal": { + type: "oauth", + provider: "openai-codex", + access: "access", + refresh: "refresh", + expires: Date.now() + 60_000, + }, + "openai:backup": { + type: "api_key", + provider: "openai", + key: "sk-platform", + }, + }, + }; + + const order = resolveAuthProfileOrder({ + cfg: { + auth: { + order: { + openai: ["openai-codex:personal", "openai:backup"], + }, + }, + }, + store, + provider: "openai-codex", + }); + + expect(order).toEqual(["openai-codex:personal", "openai:backup"]); + }); + it("keeps direct OpenAI Codex auth order ahead of the friendly OpenAI alias", async () => { const store: AuthProfileStore = { version: 1, diff --git a/src/agents/btw.ts b/src/agents/btw.ts index 95ce64084e3..5988b48aec0 100644 --- a/src/agents/btw.ts +++ b/src/agents/btw.ts @@ -256,6 +256,7 @@ async function resolveRuntimeModel(params: { agentId: params.agentId, sessionKey: params.sessionKey, }).runtime, + config: params.cfg, }), agentDir: params.agentDir, sessionEntry: params.sessionEntry, diff --git a/src/agents/command/attempt-execution.ts b/src/agents/command/attempt-execution.ts index aef2d9ef2a6..7fe255c00b3 100644 --- a/src/agents/command/attempt-execution.ts +++ b/src/agents/command/attempt-execution.ts @@ -463,6 +463,11 @@ export function runAgentAttempt(params: { config: params.cfg, workspaceDir: params.workspaceDir, }); + const embeddedPiHarnessOverride = + requestedAgentHarnessId ?? + (agentHarnessPolicy.runtime === "pi" && embeddedPiProvider !== params.providerOverride + ? "pi" + : undefined); if (!isRawModelRun && isCliProvider(cliExecutionProvider, params.cfg)) { const cliSessionBinding = getCliSessionBinding(params.sessionEntry, cliExecutionProvider); const resolveReusableCliSessionBinding = async () => { @@ -609,7 +614,8 @@ export function runAgentAttempt(params: { sessionFile: params.sessionFile, workspaceDir: params.workspaceDir, config: params.cfg, - agentHarnessId: requestedAgentHarnessId, + agentHarnessId: embeddedPiHarnessOverride, + agentHarnessRuntimeOverride: embeddedPiHarnessOverride, skillsSnapshot: params.skillsSnapshot, prompt: effectivePrompt, images: params.isFallbackRetry ? undefined : params.opts.images, diff --git a/src/agents/model-auth-label.ts b/src/agents/model-auth-label.ts index fe5c5320b13..287872685c2 100644 --- a/src/agents/model-auth-label.ts +++ b/src/agents/model-auth-label.ts @@ -7,6 +7,7 @@ import { resolveAuthProfileDisplayLabel, resolveAuthProfileOrder, } from "./auth-profiles.js"; +import { isStoredCredentialCompatibleWithAuthProvider } from "./auth-profiles/order.js"; import { readClaudeCliCredentialsCached, readCodexCliCredentialsCached, @@ -21,6 +22,7 @@ export function resolveModelAuthLabel(params: { agentDir?: string; workspaceDir?: string; includeExternalProfiles?: boolean; + acceptedProviderIds?: readonly string[]; }): string | undefined { const resolvedProvider = params.provider?.trim(); if (!resolvedProvider) { @@ -39,17 +41,37 @@ export function resolveModelAuthLabel(params: { }), }); const profileOverride = params.sessionEntry?.authProfileOverride?.trim(); - const order = resolveAuthProfileOrder({ - cfg: params.cfg, - store, - provider: providerKey, - preferredProfile: profileOverride, - }); + const acceptedProviderKeys = [ + ...new Set( + [...(params.acceptedProviderIds ?? []).map(normalizeProviderId), providerKey].filter(Boolean), + ), + ]; + const order = [ + ...new Set( + acceptedProviderKeys.flatMap((acceptedProvider) => + resolveAuthProfileOrder({ + cfg: params.cfg, + store, + provider: acceptedProvider, + preferredProfile: profileOverride, + }), + ), + ), + ]; const candidates = [profileOverride, ...order].filter(Boolean) as string[]; for (const profileId of candidates) { const profile = store.profiles[profileId]; - if (!profile || normalizeProviderId(profile.provider) !== providerKey) { + if ( + !profile || + !acceptedProviderKeys.some((acceptedProvider) => + isStoredCredentialCompatibleWithAuthProvider({ + cfg: params.cfg, + provider: acceptedProvider, + credential: profile, + }), + ) + ) { continue; } const label = resolveAuthProfileDisplayLabel({ diff --git a/src/agents/openai-codex-routing.test.ts b/src/agents/openai-codex-routing.test.ts index 852f5503d00..aabb02508d7 100644 --- a/src/agents/openai-codex-routing.test.ts +++ b/src/agents/openai-codex-routing.test.ts @@ -5,6 +5,7 @@ import { modelSelectionShouldEnsureCodexPlugin, openAIProviderUsesCodexRuntimeByDefault, resolveOpenAIRuntimeProviderForPi, + resolveSelectedOpenAIPiRuntimeProvider, } from "./openai-codex-routing.js"; describe("OpenAI Codex routing policy", () => { @@ -51,6 +52,96 @@ describe("OpenAI Codex routing policy", () => { ).toBe("openai-codex"); }); + it("keeps explicit OpenAI PI Codex auth order ahead of API-key backups", () => { + const config = { + auth: { + order: { + openai: ["openai-codex:work", "openai:backup"], + }, + }, + } satisfies OpenClawConfig; + + expect( + listOpenAIAuthProfileProvidersForAgentRuntime({ + provider: "openai", + harnessRuntime: "pi", + config, + }), + ).toEqual(["openai-codex", "openai"]); + expect( + resolveSelectedOpenAIPiRuntimeProvider({ + provider: "openai", + harnessRuntime: "pi", + config, + }), + ).toBe("openai-codex"); + expect( + resolveOpenAIRuntimeProviderForPi({ + provider: "openai", + harnessRuntime: "pi", + config, + }), + ).toBe("openai"); + }); + + it("keeps explicit OpenAI PI API-key auth order ahead of Codex backups", () => { + const config = { + auth: { + order: { + openai: ["openai:backup", "openai-codex:work"], + }, + }, + } satisfies OpenClawConfig; + + expect( + listOpenAIAuthProfileProvidersForAgentRuntime({ + provider: "openai", + harnessRuntime: "pi", + config, + }), + ).toEqual(["openai", "openai-codex"]); + expect( + resolveSelectedOpenAIPiRuntimeProvider({ + provider: "openai", + harnessRuntime: "pi", + config, + }), + ).toBe("openai"); + }); + + it("does not route custom OpenAI-compatible PI configs through Codex auth order", () => { + const config = { + models: { + providers: { + openai: { + baseUrl: "https://proxy.example.test/v1", + models: [], + }, + }, + }, + auth: { + order: { + openai: ["openai-codex:work", "openai:backup"], + }, + }, + } satisfies OpenClawConfig; + + expect( + listOpenAIAuthProfileProvidersForAgentRuntime({ + provider: "openai", + harnessRuntime: "pi", + config, + }), + ).toEqual(["openai", "openai-codex"]); + expect( + resolveSelectedOpenAIPiRuntimeProvider({ + provider: "openai", + harnessRuntime: "pi", + config, + }), + ).toBe("openai"); + }); + it("validates Codex harness auth through the Codex provider contract", () => { expect( listOpenAIAuthProfileProvidersForAgentRuntime({ diff --git a/src/agents/openai-codex-routing.ts b/src/agents/openai-codex-routing.ts index c88360b4608..ea0decb8511 100644 --- a/src/agents/openai-codex-routing.ts +++ b/src/agents/openai-codex-routing.ts @@ -2,7 +2,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { normalizeEmbeddedAgentRuntime } from "./pi-embedded-runner/runtime.js"; import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; -import { normalizeProviderId } from "./provider-id.js"; +import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js"; export const OPENAI_PROVIDER_ID = "openai"; export const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; @@ -78,6 +78,20 @@ export function hasOpenAICodexAuthProfileOverride(value: unknown): boolean { ); } +function configuredOpenAIAuthOrderStartsWithCodexProfile(config: OpenClawConfig | undefined) { + if (!openAIProviderUsesCodexRuntimeByDefault({ provider: OPENAI_PROVIDER_ID, config })) { + return false; + } + const configuredOpenAIOrder = findNormalizedProviderValue( + config?.auth?.order, + OPENAI_PROVIDER_ID, + ); + const firstProfile = configuredOpenAIOrder?.find( + (profileId) => typeof profileId === "string" && profileId.trim().length > 0, + ); + return hasOpenAICodexAuthProfileOverride(firstProfile); +} + export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: { provider: string; harnessRuntime?: string; @@ -87,16 +101,16 @@ export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: { config?: OpenClawConfig; workspaceDir?: string; }): boolean { - if ( - !isOpenAIProvider(params.provider) || - !hasOpenAICodexAuthProfileOverride(params.authProfileId) - ) { + if (!isOpenAIProvider(params.provider)) { return false; } const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime); if (runtime !== "pi") { return false; } + if (!hasOpenAICodexAuthProfileOverride(params.authProfileId)) { + return false; + } const aliasLookupParams = { config: params.config, workspaceDir: params.workspaceDir, @@ -112,6 +126,7 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: { provider: string; harnessRuntime?: string; agentHarnessId?: string; + config?: OpenClawConfig; }): string[] { if (!isOpenAIProvider(params.provider)) { return [params.provider]; @@ -123,6 +138,9 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: { return [OPENAI_CODEX_PROVIDER_ID]; } if (runtime === "pi") { + if (configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)) { + return [OPENAI_CODEX_PROVIDER_ID, OPENAI_PROVIDER_ID]; + } return [OPENAI_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID]; } return [params.provider]; @@ -150,6 +168,27 @@ export function resolveOpenAIRuntimeProviderForPi(params: { : params.provider; } +export function resolveSelectedOpenAIPiRuntimeProvider(params: { + provider: string; + harnessRuntime?: string; + agentHarnessId?: string; + authProfileProvider?: string; + authProfileId?: string; + config?: OpenClawConfig; + workspaceDir?: string; +}): string { + if (shouldRouteOpenAIPiThroughCodexAuthProvider(params)) { + return OPENAI_CODEX_PROVIDER_ID; + } + const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime); + return isOpenAIProvider(params.provider) && + runtime === "pi" && + !params.authProfileId?.trim() && + configuredOpenAIAuthOrderStartsWithCodexProfile(params.config) + ? OPENAI_CODEX_PROVIDER_ID + : params.provider; +} + export function resolveContextConfigProviderForRuntime(params: { provider: string; runtimeId?: string; diff --git a/src/agents/pi-embedded-runner.e2e.test.ts b/src/agents/pi-embedded-runner.e2e.test.ts index 6681c245b99..e73abe8e89b 100644 --- a/src/agents/pi-embedded-runner.e2e.test.ts +++ b/src/agents/pi-embedded-runner.e2e.test.ts @@ -343,6 +343,69 @@ describe("runEmbeddedPiAgent", () => { expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); }); + it("resolves explicit OpenAI PI runs through Codex when auth order starts with Codex OAuth", async () => { + const sessionFile = nextSessionFile(); + const cfg = { + ...createEmbeddedPiRunnerOpenAiConfig(["mock-1"]), + agents: { + defaults: { + models: { + "openai/mock-1": { + agentRuntime: { id: "pi" }, + }, + }, + }, + }, + auth: { + order: { + openai: ["openai-codex:work", "openai:backup"], + }, + }, + }; + runEmbeddedAttemptMock.mockResolvedValueOnce( + makeEmbeddedRunnerAttempt({ + assistantTexts: ["ok"], + lastAssistant: buildEmbeddedRunnerAssistant({ + content: [{ type: "text", text: "ok" }], + }), + }), + ); + + await runEmbeddedPiAgent({ + sessionId: "codex-first-pi", + sessionFile, + workspaceDir, + config: cfg, + prompt: "hello", + provider: "openai", + model: "mock-1", + timeoutMs: 5_000, + agentDir, + runId: nextRunId("codex-first-pi"), + enqueue: immediateEnqueue, + }); + + expect(resolveModelAsyncMock).toHaveBeenNthCalledWith( + 1, + "openai", + "mock-1", + agentDir, + cfg, + expect.objectContaining({ skipPiDiscovery: true }), + ); + expect(resolveModelAsyncMock).toHaveBeenNthCalledWith( + 2, + "openai-codex", + "mock-1", + agentDir, + cfg, + expect.objectContaining({ skipPiDiscovery: true }), + ); + expect( + (firstRunEmbeddedAttemptParams() as { model?: { provider?: string } }).model?.provider, + ).toBe("openai-codex"); + }); + it("backfills a trimmed session key from sessionId when the embedded run omits it", async () => { const sessionFile = nextSessionFile(); const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 6788126e263..d34c0ae6374 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -61,7 +61,11 @@ import { shouldPreferExplicitConfigApiKeyAuth, } from "../model-auth.js"; import { ensureOpenClawModelsJson } from "../models-config.js"; -import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js"; +import { + listOpenAIAuthProfileProvidersForAgentRuntime, + resolveContextConfigProviderForRuntime, + resolveSelectedOpenAIPiRuntimeProvider, +} from "../openai-codex-routing.js"; import { retireSessionMcpRuntime, retireSessionMcpRuntimeForSessionKey, @@ -552,6 +556,16 @@ export async function runEmbeddedPiAgent( agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride, }); const pluginHarnessOwnsTransport = agentHarness.id !== "pi"; + const modelConfigProvider = provider; + const selectedPiRuntimeProvider = resolveSelectedOpenAIPiRuntimeProvider({ + provider, + harnessRuntime: agentHarness.id, + agentHarnessId: agentHarness.id, + authProfileProvider: params.authProfileId?.split(":", 1)[0], + authProfileId: params.authProfileId, + config: params.config, + workspaceDir: resolvedWorkspace, + }); const dynamicModelResolution = await resolveModelAsync( provider, modelId, @@ -565,7 +579,7 @@ export async function runEmbeddedPiAgent( workspaceDir: resolvedWorkspace, }, ); - const modelResolution = + let modelResolution = dynamicModelResolution.model || pluginHarnessOwnsTransport ? dynamicModelResolution : await (async () => { @@ -576,6 +590,22 @@ export async function runEmbeddedPiAgent( workspaceDir: resolvedWorkspace, }); })(); + if (selectedPiRuntimeProvider !== provider && modelResolution.model) { + const runtimeModelResolution = await resolveModelAsync( + selectedPiRuntimeProvider, + modelId, + agentDir, + params.config, + { + skipPiDiscovery: true, + workspaceDir: resolvedWorkspace, + }, + ); + if (runtimeModelResolution.model) { + provider = selectedPiRuntimeProvider; + modelResolution = runtimeModelResolution; + } + } const { model, error, authStorage, modelRegistry } = modelResolution; if (!model) { throw new FailoverError(error ?? `Unknown model: ${provider}/${modelId}`, { @@ -592,7 +622,7 @@ export async function runEmbeddedPiAgent( cfg: params.config, provider, contextConfigProvider: resolveContextConfigProviderForRuntime({ - provider, + provider: modelConfigProvider, runtimeId: agentHarness.id, }), modelId, @@ -719,12 +749,23 @@ export async function runEmbeddedPiAgent( } const profileOrder = shouldPreferExplicitConfigApiKeyAuth(params.config, provider) ? [] - : resolveAuthProfileOrder({ - cfg: params.config, - store: authStore, - provider, - preferredProfile: preferredProfileId, - }); + : [ + ...new Set( + listOpenAIAuthProfileProvidersForAgentRuntime({ + provider, + harnessRuntime: agentHarness.id, + agentHarnessId: agentHarness.id, + config: params.config, + }).flatMap((authProvider) => + resolveAuthProfileOrder({ + cfg: params.config, + store: authStore, + provider: authProvider, + preferredProfile: preferredProfileId, + }), + ), + ), + ]; const providerPreferredProfileId = lockedProfileId ? undefined : resolveProviderAuthProfileId({ diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 71f5f6c2095..6d2a71b2a0c 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -1791,6 +1791,11 @@ export async function runAgentTurnWithFallback(params: { config: runtimeConfig, workspaceDir: params.followupRun.run.workspaceDir, }); + const embeddedRunHarnessOverride = + sessionRuntimeOverride ?? + (agentHarnessPolicy.runtime === "pi" && embeddedRunProvider !== provider + ? "pi" + : undefined); return (async () => { let attemptCompactionCount = 0; const lifecycleBackstop = createEmbeddedLifecycleTerminalBackstop({ @@ -1810,8 +1815,8 @@ export async function runAgentTurnWithFallback(params: { ...senderContext, ...runBaseParams, provider: embeddedRunProvider, - agentHarnessId: sessionRuntimeOverride, - agentHarnessRuntimeOverride: sessionRuntimeOverride, + agentHarnessId: embeddedRunHarnessOverride, + agentHarnessRuntimeOverride: embeddedRunHarnessOverride, sandboxSessionKey: params.runtimePolicySessionKey, prompt: params.commandBody, transcriptPrompt: params.transcriptCommandBody, diff --git a/src/auto-reply/reply/commands-status.test.ts b/src/auto-reply/reply/commands-status.test.ts index fd1d369b0af..88b04e99c21 100644 --- a/src/auto-reply/reply/commands-status.test.ts +++ b/src/auto-reply/reply/commands-status.test.ts @@ -716,6 +716,87 @@ describe("buildStatusReply subagent summary", () => { ); }); + it("uses Codex OAuth auth labels for explicit OpenAI PI auth order", async () => { + await withTempHome( + async (dir) => { + const authPath = path.join( + dir, + ".openclaw", + "agents", + "main", + "agent", + "auth-profiles.json", + ); + fs.mkdirSync(path.dirname(authPath), { recursive: true }); + fs.writeFileSync( + authPath, + JSON.stringify({ + version: 1, + profiles: { + "openai-codex:status": { + type: "oauth", + provider: "openai-codex", + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60 * 60_000, + }, + "openai:backup": { + type: "api_key", + provider: "openai", + key: "sk-test", + }, + }, + }), + "utf8", + ); + + const text = await buildStatusText({ + cfg: { + ...baseCfg, + agents: { + defaults: { + models: { + "openai/gpt-5.5": { + agentRuntime: { id: "pi" }, + }, + }, + }, + }, + auth: { + order: { + openai: ["openai-codex:status", "openai:backup"], + }, + }, + }, + sessionEntry: { + sessionId: "sess-status-openai-pi-codex-oauth", + updatedAt: 0, + }, + sessionKey: "agent:main:main", + parentSessionKey: "agent:main:main", + sessionScope: "per-sender", + statusChannel: "mobilechat", + provider: "openai", + model: "gpt-5.5", + contextTokens: 32_000, + resolvedHarness: "pi", + resolvedFastMode: false, + resolvedVerboseLevel: "off", + resolvedReasoningLevel: "off", + resolveDefaultThinkingLevel: async () => undefined, + isGroup: false, + defaultGroupActivation: () => "mention", + }); + + const normalized = normalizeTestText(text); + expect(normalized).toContain("Model: openai/gpt-5.5"); + expect(normalized).toContain("oauth (openai-codex:status)"); + expect(normalized).not.toContain("api-key (openai:backup)"); + }, + { env: { OPENAI_API_KEY: undefined } }, + ); + }); + it("uses Claude CLI OAuth auth labels for anthropic models running on the Claude CLI runtime", async () => { await withTempHome( async (dir) => { diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 2eaf4c27160..e918f279de9 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -880,6 +880,7 @@ export async function runPreparedReply( ? listOpenAIAuthProfileProvidersForAgentRuntime({ provider, harnessRuntime: agentHarnessPolicy.runtime, + config: cfg, }) : [provider]; const resolveActiveSessionProviderForAuthProfile = (): string => { diff --git a/src/auto-reply/reply/model-selection.ts b/src/auto-reply/reply/model-selection.ts index 2523d27d1dc..48627c6f126 100644 --- a/src/auto-reply/reply/model-selection.ts +++ b/src/auto-reply/reply/model-selection.ts @@ -328,6 +328,7 @@ export async function createModelSelectionState(params: { const acceptedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({ provider, harnessRuntime: harnessPolicy.runtime, + config: cfg, }).map(normalizeProviderId); if (!profile || !acceptedAuthProviders.includes(profileProvider ?? "")) { await clearSessionAuthProfileOverride({ diff --git a/src/cli/models-cli.test.ts b/src/cli/models-cli.test.ts index 9963c18db3f..b3f8472d77d 100644 --- a/src/cli/models-cli.test.ts +++ b/src/cli/models-cli.test.ts @@ -209,6 +209,22 @@ describe("models cli", () => { }); }); + it("maps --device-code to the provider device-code auth method", async () => { + await runModelsCommand([ + "models", + "auth", + "login", + "--provider", + "openai-codex", + "--device-code", + ]); + + expectCommandOptions(modelsAuthLoginCommand, { + provider: "openai-codex", + method: "device-code", + }); + }); + it("passes list-specific --agent and --json to models auth list", async () => { await runModelsCommand(["models", "auth", "list", "--agent", "poe", "--json"]); diff --git a/src/cli/models-cli.ts b/src/cli/models-cli.ts index 8c14362f8b7..141e97134ed 100644 --- a/src/cli/models-cli.ts +++ b/src/cli/models-cli.ts @@ -332,15 +332,21 @@ export function registerModelsCli(program: Command) { .description("Run a provider plugin auth flow (OAuth/API key)") .option("--provider ", "Provider id registered by a plugin") .option("--method ", "Provider auth method id") + .option("--device-code", "Use the provider device-code auth method", false) .option("--set-default", "Apply the provider's default model recommendation", false) .action(async (opts, command) => { + if (opts.deviceCode && typeof opts.method === "string" && opts.method !== "device-code") { + throw new Error( + "--device-code cannot be combined with --method unless method is device-code.", + ); + } await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => { const agent = resolveModelAgentOption(command); const { modelsAuthLoginCommand } = await import("../commands/models/auth.js"); await modelsAuthLoginCommand( { provider: opts.provider as string | undefined, - method: opts.method as string | undefined, + method: opts.deviceCode ? "device-code" : (opts.method as string | undefined), setDefault: Boolean(opts.setDefault), agent, }, diff --git a/src/commands/auth-choice.model-check.ts b/src/commands/auth-choice.model-check.ts index 4a7cba26d41..49952c2a87d 100644 --- a/src/commands/auth-choice.model-check.ts +++ b/src/commands/auth-choice.model-check.ts @@ -26,6 +26,7 @@ function resolveAuthProviderCandidates(params: { ...listOpenAIAuthProfileProvidersForAgentRuntime({ provider: params.provider, harnessRuntime: harnessPolicy.runtime, + config: params.config, }), ]), ]; diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 71c938b9479..569405409ca 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -794,6 +794,7 @@ async function prepareCronRunContext(params: { agentId, sessionKey: agentSessionKey, }).runtime, + config: cfgWithAgentDefaults, }), agentDir, sessionEntry: cronSession.sessionEntry, diff --git a/src/status/status-text.ts b/src/status/status-text.ts index 165722c213c..88fce23a5be 100644 --- a/src/status/status-text.ts +++ b/src/status/status-text.ts @@ -11,6 +11,7 @@ import { resolveContextTokensForModel } from "../agents/context.js"; import { resolveFastModeState } from "../agents/fast-mode.js"; import { resolveModelAuthLabel } from "../agents/model-auth-label.js"; import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js"; +import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../agents/openai-codex-routing.js"; import { resolveInternalSessionKey, resolveMainSessionAlias, @@ -147,7 +148,7 @@ async function resolveStatusHarnessId(params: { agentHarnessId: params.sessionEntry?.agentHarnessId, }); const id = normalizeOptionalLowercaseString(selected.id); - return id && id !== "pi" ? id : undefined; + return id || undefined; } catch { return undefined; } @@ -234,15 +235,26 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise