From d92b3b5cc2bd17bf13058172a7458b446d2aa7af Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 11:48:41 +0200 Subject: [PATCH] refactor: unify OpenAI provider identity Refactor OpenAI provider identity so OpenAI remains the canonical provider for API-key and OAuth-backed flows while legacy openai-codex state is doctor/migration-only. Keeps OpenAI Codex Responses as an API/transport class rather than a provider identity, moves auth aliases through providerAuthAliases, updates doctor repair sequencing for old auth/profile state, and refreshes tests/docs around the canonical OpenAI behavior. --- docs/channels/discord.md | 14 +- docs/cli/doctor.md | 2 +- docs/cli/infer.md | 4 +- docs/cli/models.md | 24 +- docs/concepts/agent-runtimes.md | 10 +- docs/concepts/model-failover.md | 7 +- docs/concepts/model-providers.md | 18 +- docs/concepts/models.md | 4 +- docs/concepts/oauth.md | 6 +- docs/gateway/authentication.md | 4 +- docs/gateway/config-agents.md | 2 +- docs/gateway/configuration-examples.md | 5 +- docs/gateway/configuration-reference.md | 4 +- docs/gateway/doctor.md | 8 +- docs/gateway/logging.md | 2 +- docs/help/faq-first-run.md | 12 +- docs/help/faq-models.md | 4 +- docs/help/testing-live.md | 10 +- docs/nodes/media-understanding.md | 10 +- docs/plugins/codex-harness.md | 13 +- docs/plugins/manifest.md | 2 +- docs/plugins/memory-lancedb.md | 4 +- docs/plugins/plugin-inventory.md | 2 +- docs/plugins/reference.md | 2 +- docs/plugins/reference/openai.md | 2 +- docs/plugins/sdk-provider-plugins.md | 2 +- docs/providers/openai.md | 108 +- docs/tools/acp-agents.md | 8 +- docs/tools/image-generation.md | 14 +- docs/tools/llm-task.md | 4 +- docs/tools/plugin.md | 2 +- docs/tools/thinking.md | 2 +- docs/tools/web.md | 2 +- docs/web/control-ui.md | 2 +- .../codex/src/app-server/auth-bridge.ts | 4 +- .../app-server/local-runtime-attribution.ts | 3 +- .../codex/src/app-server/session-binding.ts | 2 +- extensions/codex/src/command-account.ts | 2 +- extensions/codex/src/migration/auth.ts | 7 +- .../codex/src/migration/provider.test.ts | 66 +- extensions/llm-task/README.md | 6 +- extensions/llm-task/index.ts | 2 +- extensions/llm-task/src/llm-task-tool.test.ts | 14 +- extensions/llm-task/src/llm-task-tool.ts | 2 +- extensions/migrate-hermes/auth.ts | 25 +- .../provider.secret-failure.test.ts | 10 +- extensions/migrate-hermes/secrets.test.ts | 78 +- extensions/openai/api.ts | 3 +- .../openai/image-generation-provider.test.ts | 221 +++- .../openai/image-generation-provider.ts | 301 +++-- extensions/openai/index.test.ts | 22 +- extensions/openai/index.ts | 8 +- .../media-understanding-provider.test.ts | 32 +- .../openai/media-understanding-provider.ts | 24 +- .../openai/openai-codex-oauth.runtime.ts | 2 +- .../openai/openai-codex-provider.runtime.ts | 4 +- .../openai/openai-codex-provider.test.ts | 1005 ++--------------- extensions/openai/openai-codex-provider.ts | 198 ++-- extensions/openai/openai-provider.test.ts | 181 ++- extensions/openai/openai-provider.ts | 179 ++- extensions/openai/openclaw.plugin.json | 166 +-- extensions/openai/openclaw.plugin.test.ts | 55 +- extensions/openai/prompt-overlay.ts | 2 +- extensions/openai/provider-contract-api.ts | 58 +- extensions/openai/provider-policy-api.test.ts | 18 + .../openai/realtime-transcription-provider.ts | 4 +- extensions/openai/realtime-voice-provider.ts | 6 +- extensions/openai/register.runtime.ts | 6 +- extensions/openai/setup-api.test.ts | 10 +- extensions/openai/setup-api.ts | 81 +- extensions/openai/test-api.ts | 5 +- extensions/openai/transport-policy.ts | 6 +- .../openai/video-generation-provider.test.ts | 67 +- .../openai/video-generation-provider.ts | 5 +- .../auth-profile-runtime-contract.test.ts | 34 +- .../auth-profiles.external-cli-sync.test.ts | 4 +- src/agents/auth-profiles/constants.ts | 6 +- src/agents/auth-profiles/external-cli-sync.ts | 299 +++-- .../auth-profiles/external-oauth.test.ts | 25 + .../auth-profiles/oauth-refresh-failure.ts | 18 +- ...auth.openai-codex-refresh-fallback.test.ts | 2 +- src/agents/auth-profiles/oauth.ts | 2 +- src/agents/auth-profiles/profiles.ts | 22 +- src/agents/auth-profiles/usage.ts | 5 +- src/agents/codex-native-web-search-core.ts | 36 +- .../command/attempt-execution.cli.test.ts | 8 +- src/agents/command/attempt-execution.ts | 4 +- .../compact.hooks.test.ts | 49 +- .../compaction-runtime-context.test.ts | 30 +- .../embedded-agent-runner/extra-params.ts | 1 + .../model.forward-compat.test.ts | 1 + .../model.skip-agent-discovery-hooks.test.ts | 1 + .../model.startup-retry.test.ts | 1 + src/agents/embedded-agent-runner/model.ts | 86 +- .../run.incomplete-turn.test.ts | 2 +- .../run.overflow-compaction.test.ts | 126 +-- src/agents/embedded-agent-runner/run.ts | 12 +- .../stream-resolution.test.ts | 32 +- .../stream-resolution.ts | 4 +- src/agents/execution-contract.ts | 10 +- src/agents/gpt5-prompt-overlay.ts | 2 +- src/agents/harness/policy.ts | 11 +- src/agents/live-model-filter.test.ts | 34 +- src/agents/live-model-filter.ts | 9 +- src/agents/model-auth.profiles.test.ts | 74 +- src/agents/model-auth.ts | 115 +- src/agents/model-catalog-visibility.test.ts | 39 + src/agents/model-catalog-visibility.ts | 46 +- src/agents/model-catalog.ts | 1 + src/agents/model-catalog.types.ts | 3 +- src/agents/model-compat.test.ts | 3 +- src/agents/model-provider-auth.test.ts | 2 + src/agents/model-provider-auth.ts | 27 +- src/agents/model-selection-shared.ts | 1 + src/agents/model-selection.test.ts | 5 + src/agents/openai-codex-routing.test.ts | 65 +- src/agents/openai-codex-routing.ts | 126 +-- src/agents/openai-reasoning-compat.ts | 3 +- src/agents/openai-reasoning-effort.ts | 5 +- src/agents/openai-responses-payload-policy.ts | 4 +- src/agents/openai-strict-tool-setting.ts | 1 - src/agents/openai-transport-stream.test.ts | 32 +- src/agents/openai-transport-stream.ts | 3 +- src/agents/openclaw-tools.update-plan.test.ts | 1 - .../prompt-overlay-runtime-contract.test.ts | 3 +- src/agents/provider-attribution.test.ts | 29 +- src/agents/provider-attribution.ts | 52 +- src/agents/provider-transport-fetch.test.ts | 17 + src/agents/provider-transport-fetch.ts | 50 +- src/agents/runtime-plan/auth.ts | 21 +- src/agents/runtime-plan/build.test.ts | 12 +- ...imple-completion-runtime.selection.test.ts | 4 +- src/agents/simple-completion-runtime.test.ts | 8 +- src/agents/simple-completion-runtime.ts | 11 +- .../simple-completion-transport.test.ts | 4 +- src/agents/simple-completion-transport.ts | 2 +- src/agents/tools/image-tool.test.ts | 39 +- .../tools/pdf-tool.model-config.test.ts | 2 +- src/agents/tools/video-generate-tool.test.ts | 6 +- src/agents/tools/video-generate-tool.ts | 24 +- .../reply/agent-runner-execution.test.ts | 12 +- .../reply/agent-runner-execution.ts | 9 +- src/auto-reply/reply/commands-models.test.ts | 4 +- src/commands/auth-choice-legacy.test.ts | 17 +- src/commands/auth-choice-legacy.ts | 11 +- src/commands/auth-choice.apply.ts | 2 +- src/commands/auth-choice.model-check.test.ts | 5 +- src/commands/auth-choice.test.ts | 5 +- .../doctor-auth-flat-profiles.test.ts | 415 ++++++- src/commands/doctor-auth-flat-profiles.ts | 361 ++++++ src/commands/doctor-auth.hints.test.ts | 6 +- src/commands/doctor-auth.ts | 2 +- src/commands/doctor/repair-sequencing.ts | 23 + .../shared/codex-route-warnings.test.ts | 17 + .../doctor/shared/codex-route-warnings.ts | 16 + src/commands/migrate.test.ts | 2 +- .../models.auth.provider-resolution.test.ts | 2 + src/commands/models/auth-list.test.ts | 2 +- src/commands/models/auth-list.ts | 2 +- src/commands/models/auth.test.ts | 43 +- src/commands/models/auth.ts | 23 +- src/commands/models/list.auth-index.ts | 20 +- src/commands/models/list.probe.test.ts | 2 - src/commands/models/list.probe.ts | 3 - src/commands/models/list.status-command.ts | 110 +- src/commands/models/list.status.test.ts | 10 +- .../oauth-tls-preflight.doctor.test.ts | 29 + .../local/auth-choice.test.ts | 40 +- .../local/auth-choice.ts | 28 +- src/commands/provider-auth-guidance.ts | 3 +- .../server-methods/models-auth-status.ts | 9 +- src/gateway/session-utils.test.ts | 18 +- src/gateway/sessions-patch.test.ts | 2 +- src/infra/provider-usage.shared.test.ts | 6 + src/infra/provider-usage.shared.ts | 11 +- .../providers/stream-wrappers/openai.test.ts | 4 +- src/llm/providers/stream-wrappers/openai.ts | 15 +- src/llm/utils/oauth/openai-codex.test.ts | 4 +- src/llm/utils/oauth/openai-codex.ts | 2 +- src/plugin-sdk/provider-auth.ts | 12 +- src/plugin-sdk/provider-tools.test.ts | 25 + src/plugin-sdk/provider-tools.ts | 8 +- .../agents/auth-profile-runtime-contract.ts | 15 +- .../plugin-registration-contract-cases.ts | 4 +- .../test-helpers/provider-auth-contract.ts | 16 +- .../test-helpers/provider-runtime-contract.ts | 10 +- src/plugins/plugin-lookup-table.test.ts | 6 +- src/plugins/plugin-metadata-snapshot.ts | 14 + src/plugins/plugin-registry-id-normalizer.ts | 1 + .../provider-openai-codex-oauth-tls.ts | 4 +- src/plugins/provider-openai-codex-oauth.ts | 2 +- src/plugins/provider-public-artifacts.test.ts | 63 ++ src/plugins/provider-public-artifacts.ts | 27 +- src/plugins/provider-runtime.test-support.ts | 7 +- src/plugins/provider-runtime.test.ts | 3 - src/plugins/providers.test.ts | 18 + src/plugins/providers.ts | 11 + src/plugins/setup-descriptors.ts | 13 +- src/plugins/setup-registry.test.ts | 46 + src/plugins/types.ts | 4 + src/test-utils/plugin-registration.ts | 17 +- 201 files changed, 3959 insertions(+), 2587 deletions(-) create mode 100644 extensions/openai/provider-policy-api.test.ts diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 035d5e199625..68a585307f42 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -354,7 +354,7 @@ To restrict who can click a button, set `allowedUsers` on that button (Discord u Component callbacks expire after 30 minutes by default. Set `channels.discord.agentComponents.ttlMs` to change that callback registry lifetime for the default Discord account, or `channels.discord.accounts..agentComponents.ttlMs` to override one account in a multi-account setup. The value is milliseconds, must be a positive integer, and is capped at `86400000` (24 hours). Longer TTLs are useful for review or approval workflows that need buttons to remain usable, but they also extend the window where an old Discord message can still trigger an action. Prefer the shortest TTL that fits the workflow, and keep the default when stale callbacks would be surprising. -The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai-codex` or `vllm`. +The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai` or `vllm`. File attachments: @@ -1197,7 +1197,7 @@ Auto-join example: discord: { voice: { enabled: true, - model: "openai-codex/gpt-5.5", + model: "openai/gpt-5.5", autoJoin: [ { guildId: "123456789012345678", @@ -1234,7 +1234,7 @@ Notes: - `voice.followUsers` lets the bot join, move, and leave Discord voice with selected users. See [Follow users in voice](#follow-users-in-voice) for behavior rules and examples. - `agent-proxy` routes speech through `discord-voice`, which preserves normal owner/tool authorization for the speaker and target session but hides the agent `tts` tool because Discord voice owns playback. By default, `agent-proxy` gives the consult full owner-equivalent tool access for owner speakers (`voice.realtime.toolPolicy: "owner"`) and strongly prefers consulting the OpenClaw agent before substantive answers (`voice.realtime.consultPolicy: "always"`). In that default `always` mode, the realtime layer does not auto-speak filler before the consult answer; it captures and transcribes speech, then speaks the routed OpenClaw answer. If multiple forced consult answers finish while Discord is still playing the first answer, later exact-speech answers are queued until playback idles instead of replacing speech mid-sentence. - In `stt-tts` mode, STT uses `tools.media.audio`; `voice.model` does not affect transcription. -- In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.speakerVoice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai-codex/gpt-5.5"`. +- In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.speakerVoice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai/gpt-5.5"`. - Realtime voice modes include small `IDENTITY.md`, `USER.md`, and `SOUL.md` profile files in the realtime provider instructions by default so fast direct turns keep the same identity, user grounding, and persona as the routed OpenClaw agent. Set `voice.realtime.bootstrapContextFiles` to a subset to customize this, or `[]` to disable it. The supported realtime bootstrap files are limited to those profile files; `AGENTS.md` stays in the normal agent context. The injected profile context does not replace `openclaw_agent_consult` for workspace work, current facts, memory lookup, or tool-backed actions. - In OpenAI `agent-proxy` realtime mode, set `voice.realtime.requireWakeName: true` to keep Discord realtime voice silent until a transcript starts or ends with a wake name. Configured wake names must be one or two words. If `voice.realtime.wakeNames` is unset, OpenClaw uses the routed agent `name` plus `OpenClaw`, falling back to the agent id plus `OpenClaw`. Wake-name gating disables realtime provider auto-response, routes accepted turns through the OpenClaw agent consult path, and gives a short spoken acknowledgement when a leading wake name is recognized from partial transcription before the final transcript arrives. - The OpenAI realtime provider accepts current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events, so compatible provider snapshots can drift without dropping assistant audio. @@ -1325,7 +1325,7 @@ Default agent-proxy voice-channel session example: discord: { voice: { enabled: true, - model: "openai-codex/gpt-5.5", + model: "openai/gpt-5.5", followUsersEnabled: true, followUsers: ["123456789012345678"], realtime: { @@ -1375,7 +1375,7 @@ Realtime bidi example: voice: { enabled: true, mode: "bidi", - model: "openai-codex/gpt-5.5", + model: "openai/gpt-5.5", realtime: { provider: "openai", model: "gpt-realtime-2", @@ -1398,7 +1398,7 @@ Voice as an extension of an existing Discord channel session: voice: { enabled: true, mode: "agent-proxy", - model: "openai-codex/gpt-5.5", + model: "openai/gpt-5.5", agentSession: { mode: "target", target: "channel:123456789012345678", @@ -1433,7 +1433,7 @@ Echo-heavy OpenAI Realtime example: voice: { enabled: true, mode: "bidi", - model: "openai-codex/gpt-5.5", + model: "openai/gpt-5.5", realtime: { provider: "openai", model: "gpt-realtime-2", diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md index d7cec35d7fac..1cf71ec13300 100644 --- a/docs/cli/doctor.md +++ b/docs/cli/doctor.md @@ -203,7 +203,7 @@ Notes: - Doctor reports cron jobs with explicit `payload.model` overrides, including provider namespace counts and mismatches against `agents.defaults.model`, so scheduled jobs that do not inherit the default model are visible during auth or billing investigations. - On Linux, doctor warns when the user's crontab still runs legacy `~/.openclaw/bin/ensure-whatsapp.sh`; that script is no longer maintained and can log false WhatsApp gateway outages when cron lacks the systemd user-bus environment. - When WhatsApp is enabled, doctor checks for a degraded Gateway event loop with local `openclaw-tui` clients still running. `doctor --fix` stops only verified local TUI clients so WhatsApp replies are not queued behind stale TUI refresh loops. -- Doctor rewrites legacy `openai-codex/*` model refs to canonical `openai/*` refs across primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` moves Codex intent onto provider/model-scoped `agentRuntime.id: "codex"` entries, preserves session auth-profile pins such as `openai-codex:...`, removes stale whole-agent/session runtime pins, and keeps repaired OpenAI agent refs on Codex auth routing instead of direct OpenAI API-key auth. +- Doctor rewrites legacy `openai-codex/*` model refs to canonical `openai/*` refs across primary models, fallbacks, image/video generation models, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` also migrates legacy `openai-codex:*` auth profiles and `auth.order.openai-codex` entries to `openai:*`, moves Codex intent onto provider/model-scoped `agentRuntime.id: "codex"` entries, removes stale whole-agent/session runtime pins, and keeps repaired OpenAI agent refs on Codex auth routing instead of direct OpenAI API-key auth. - Doctor cleans legacy plugin dependency staging state created by older OpenClaw versions and relinks the host `openclaw` package for managed npm plugins that declare it as a peer dependency. It also repairs missing downloadable plugins that are referenced by config, such as `plugins.entries`, configured channels, configured provider/search settings, or configured agent runtimes. During package updates, doctor skips package-manager plugin repair until the package swap is complete; rerun `openclaw doctor --fix` afterward if a configured plugin still needs recovery. If the download fails, doctor reports the install error and preserves the configured plugin entry for the next repair attempt. - Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.deny`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy. - Doctor quarantines invalid plugin config by disabling the affected `plugins.entries.` entry and removing its invalid `config` payload. Gateway startup already skips only that bad plugin so other plugins and channels can keep running. diff --git a/docs/cli/infer.md b/docs/cli/infer.md index 58d11d49922c..b14ac7b8835d 100644 --- a/docs/cli/infer.md +++ b/docs/cli/infer.md @@ -129,7 +129,7 @@ This table maps common inference tasks to the corresponding infer command. - Use `model run --thinking ` to pass a one-shot thinking/reasoning level (`off`, `minimal`, `low`, `medium`, `high`, `adaptive`, `xhigh`, or `max`) while keeping the run raw. - For `image describe`, `audio transcribe`, and `video describe`, `--model` must use the form ``. - For `image describe`, `--file` accepts local paths and HTTP(S) image URLs. Remote URLs use the normal media-fetch SSRF policy. -- For `image describe`, an explicit `--model` runs that provider/model directly. The model must be image-capable in the model catalog or provider config. `codex/` runs a bounded Codex app-server image-understanding turn; `openai-codex/` uses the OpenAI Codex OAuth provider path. +- For `image describe`, an explicit `--model` runs that provider/model directly. The model must be image-capable in the model catalog or provider config. `codex/` runs a bounded Codex app-server image-understanding turn; `openai/` uses the OpenAI provider path with either API-key or ChatGPT/Codex OAuth auth. - Stateless execution commands default to local. - Gateway-managed state commands default to gateway. - The normal local path does not require the gateway to be running. @@ -172,7 +172,7 @@ Notes: - Local `model run` is the narrowest CLI smoke for provider/model/auth health because, for non-Codex providers, it sends only the supplied prompt to the selected model. - Local `model run --model ` can use exact bundled static catalog rows from `models list --all` before that provider is written to config. Provider auth is still required; missing credentials fail as auth errors, not `Unknown model`. - For Mistral Medium 3.5 reasoning probes, leave temperature unset/default. Mistral rejects `reasoning_effort="high"` plus `temperature: 0`; use `mistral/mistral-medium-3-5` with default temperature or a non-zero reasoning-mode value such as `0.7`. -- `openai-codex/*` local probes are the narrow exception: OpenClaw adds a minimal system instruction so the Codex Responses transport can populate its required `instructions` field, without adding full agent context, tools, memory, or session transcript. +- Codex Responses local probes are the narrow exception: OpenClaw adds a minimal system instruction so the transport can populate its required `instructions` field, without adding full agent context, tools, memory, or session transcript. - Local `model run --file` keeps that lean path and attaches image content directly to the single user message. Common image files such as PNG, JPEG, and WebP work when their MIME type is detected as `image/*`; unsupported or unrecognized files fail before the provider is called. - `model run --file` is best when you want to test the selected multimodal text model directly. Use `infer image describe` when you want OpenClaw's image-understanding provider selection and default image-model routing. - The selected model must support image input; text-only models may reject the request at the provider layer. diff --git a/docs/cli/models.md b/docs/cli/models.md index c3ef7863094e..dbfff69a025c 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -28,8 +28,8 @@ openclaw models scan `openclaw models status` shows the resolved default/fallbacks plus an auth overview. When provider usage snapshots are available, the OAuth/API-key status section includes provider usage windows and quota snapshots. -Current usage-window providers: Anthropic, GitHub Copilot, Gemini CLI, OpenAI -Codex, MiniMax, Xiaomi, and z.ai. Usage auth comes from provider-specific hooks +Current usage-window providers: Anthropic, GitHub Copilot, Gemini CLI, OpenAI, +MiniMax, Xiaomi, and z.ai. Usage auth comes from provider-specific hooks when available; otherwise OpenClaw falls back to matching OAuth/API-key credentials from auth profiles, env, or config. In `--json` output, `auth.providers` is the env/config/store-aware provider @@ -40,10 +40,10 @@ Use `--agent ` to inspect a configured agent's model/auth state. When omitte the command uses `OPENCLAW_AGENT_DIR` if set, otherwise the configured default agent. Probe rows can come from auth profiles, env credentials, or `models.json`. -For Codex OAuth troubleshooting, `openclaw models status`, -`openclaw models auth list --provider openai-codex`, and +For OpenAI ChatGPT/Codex OAuth troubleshooting, `openclaw models status`, +`openclaw models auth list --provider openai`, and `openclaw config get agents.defaults.model --json` are the quickest way to -confirm whether an agent has a usable `openai-codex` auth profile for +confirm whether an agent has a usable `openai` OAuth profile for `openai/*` through the native Codex runtime. See [OpenAI provider setup](/providers/openai#check-and-recover-codex-oauth-routing). Notes: @@ -76,7 +76,7 @@ Notes: cap differs from the native context window; JSON rows include `contextTokens` when a provider exposes that cap. - `models list --provider ` filters by provider id, such as `moonshot` or - `openai-codex`. It does not accept display labels from interactive provider + `openai`. It does not accept display labels from interactive provider pickers, such as `Moonshot AI`. - Model refs are parsed by splitting on the **first** `/`. If the model ID includes `/` (OpenRouter-style), include the provider prefix (example: `openrouter/moonshotai/kimi-k2`). - If you omit the provider, OpenClaw resolves the input as an alias first, then @@ -181,7 +181,7 @@ provider you choose. `models auth list` lists saved auth profiles for the selected agent without printing token, API-key, or OAuth secret material. Use `--provider ` to -filter to one provider, such as `openai-codex`, and `--json` for scripting. +filter to one provider, such as `openai`, and `--json` for scripting. `models auth login` runs a provider plugin's auth flow (OAuth/API key). Use `openclaw plugins list` to see which providers are installed. @@ -192,15 +192,15 @@ specific configured agent store. The parent `--agent` flag is honored by For OpenAI models, `--provider openai` defaults to ChatGPT/Codex account login. Use `--method api-key` only when you want to add an OpenAI API-key profile, -usually as a backup for Codex subscription limits. The legacy -`--provider openai-codex` spelling still works for existing scripts. +usually as a backup for Codex subscription limits. Run `openclaw doctor --fix` +to migrate older `openai-codex` auth/profile state to `openai`. Examples: ```bash openclaw models auth login --provider openai --set-default openclaw models auth login --provider openai --method api-key -openclaw models auth paste-api-key --provider openai-codex +openclaw models auth paste-api-key --provider openai openclaw models auth list --provider openai ``` @@ -212,7 +212,7 @@ Notes: - `paste-api-key` accepts API keys generated elsewhere, prompts for the key value, and writes it to the default profile id `:manual` unless you pass `--profile-id`. In automation, pipe the key on stdin, for example - `printf "%s\n" "$OPENAI_API_KEY" | openclaw models auth paste-api-key --provider openai-codex`. + `printf "%s\n" "$OPENAI_API_KEY" | openclaw models auth paste-api-key --provider openai`. - `setup-token` and `paste-token` remain generic token commands for providers that expose token auth methods. - `setup-token` requires an interactive TTY and runs the provider's token-auth @@ -226,7 +226,7 @@ Notes: provider credentials do not appear in shell history or process lists. - `paste-token --expires-in ` stores an absolute token expiry from a relative duration such as `365d` or `12h`. -- For `openai-codex`, OpenAI API keys and ChatGPT/OAuth token material are +- For `openai`, OpenAI API keys and ChatGPT/OAuth token material are different auth shapes. Use `paste-api-key` for `sk-...` OpenAI API keys and `paste-token` only for token auth material. - Anthropic note: Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats Claude CLI reuse and `claude -p` usage as sanctioned for this integration unless Anthropic publishes a new policy. diff --git a/docs/concepts/agent-runtimes.md b/docs/concepts/agent-runtimes.md index 62e5caf0ceff..da3f6fe0e65f 100644 --- a/docs/concepts/agent-runtimes.md +++ b/docs/concepts/agent-runtimes.md @@ -16,7 +16,7 @@ configuration. They are different layers: | Layer | Examples | What it means | | ------------- | -------------------------------------------- | ------------------------------------------------------------------- | -| Provider | `openai`, `anthropic`, `openai-codex` | How OpenClaw authenticates, discovers models, and names model refs. | +| Provider | `openai`, `anthropic`, `github-copilot` | How OpenClaw authenticates, discovers models, and names model refs. | | Model | `gpt-5.5`, `claude-opus-4-6` | The model selected for the agent turn. | | Agent runtime | `openclaw`, `codex`, `copilot`, `claude-cli` | The low level loop or backend that executes the prepared turn. | | Channel | Telegram, Discord, Slack, WhatsApp | Where messages enter and leave OpenClaw. | @@ -51,7 +51,7 @@ Most confusion comes from several different surfaces sharing the Codex name: | Surface | OpenClaw name/config | What it does | | ------------------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | Native Codex app-server runtime | `openai/*` model refs | Runs OpenAI embedded agent turns through Codex app-server. This is the usual ChatGPT/Codex subscription setup. | -| Codex OAuth auth profiles | `openai-codex` auth provider | Stores ChatGPT/Codex subscription auth that the Codex app-server harness consumes. | +| Codex OAuth auth profiles | `openai` OAuth profiles | Stores ChatGPT/Codex subscription auth that the Codex app-server harness consumes. | | Codex ACP adapter | `runtime: "acp"`, `agentId: "codex"` | Runs Codex through the external ACP/acpx control plane. Use only when ACP/acpx is explicitly asked. | | Native Codex chat-control command set | `/codex ...` | Binds, resumes, steers, stops, and inspects Codex app-server threads from chat. | | OpenAI Platform API route for non-agent surfaces | `openai/*` plus API-key auth | Used for direct OpenAI APIs such as images, embeddings, speech, and realtime. | @@ -95,7 +95,7 @@ This is the agent-facing decision tree: subscription-backed Codex agent experience, use `openai/`. 3. If the user explicitly chooses **OpenClaw for an OpenAI model**, keep the model ref as `openai/` and set provider/model runtime policy to - `agentRuntime.id: "openclaw"`. A selected `openai-codex` auth profile is routed + `agentRuntime.id: "openclaw"`. A selected `openai` OAuth profile is routed internally through OpenClaw's Codex-auth transport. 4. If legacy config still contains **`openai-codex/*` model refs**, repair it to `openai/` with `openclaw doctor --fix`; doctor keeps the Codex auth @@ -112,7 +112,7 @@ This is the agent-facing decision tree: | --------------------------------------- | -------------------------------------------- | | Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin | | Codex app-server embedded agent runtime | `openai/*` agent model refs | -| OpenAI Codex OAuth | `openai-codex` auth profiles | +| OpenAI Codex OAuth | `openai` OAuth profiles | | Claude Code or other external harness | ACP/acpx | For the OpenAI-family prefix split, see [OpenAI](/providers/openai) and @@ -196,7 +196,7 @@ backend. `auto` mode is intentionally conservative for most providers. OpenAI agent models are the exception: unset runtime and `auto` both resolve to the Codex harness. Explicit OpenClaw runtime config remains an opt-in compatibility route for -`openai/*` agent turns; when paired with a selected `openai-codex` auth profile, +`openai/*` agent turns; when paired with a selected `openai` OAuth profile, OpenClaw routes that path internally through the Codex-auth transport while keeping the public model ref as `openai/*`. Stale OpenAI runtime session pins are ignored by runtime selection and can be cleaned with `openclaw doctor --fix`. diff --git a/docs/concepts/model-failover.md b/docs/concepts/model-failover.md index bfec2096d4c0..93d7ca6b6214 100644 --- a/docs/concepts/model-failover.md +++ b/docs/concepts/model-failover.md @@ -156,15 +156,14 @@ Use `auth.order.openai` for the user-facing order: { auth: { order: { - openai: ["openai-codex:user@example.com", "openai:api-key-backup"], + openai: ["openai:user@example.com", "openai:api-key-backup"], }, }, } ``` -Existing Codex subscription profiles may still use the legacy -`openai-codex:*` profile id. The ordered API-key backup can be a normal -`openai:*` API-key profile. When the subscription hits a Codex usage limit, +Use `openai:*` for both ChatGPT/Codex OAuth profiles and OpenAI API-key +profiles. When the subscription hits a Codex usage limit, OpenClaw records the exact reset time when Codex provides one, tries the next ordered auth profile, and keeps the run inside the Codex harness. Once the reset time passes, the subscription profile is eligible again and the next automatic diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 5c294848c87d..5a1563e3f047 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -130,24 +130,24 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope } ``` -### OpenAI Codex OAuth +### OpenAI ChatGPT/Codex OAuth -- Provider: `openai-codex` +- Provider: `openai` - Auth: OAuth (ChatGPT) -- Legacy OpenAI Codex model ref: `openai-codex/gpt-5.5` +- Legacy OpenAI Codex model ref: `openai/gpt-5.5` - Native Codex app-server harness ref: `openai/gpt-5.5` - Native Codex app-server harness docs: [Codex harness](/plugins/codex-harness) - Legacy model refs: `codex/gpt-*` -- Plugin boundary: `openai-codex/*` loads the OpenAI plugin; the native Codex app-server plugin is selected only by the Codex harness runtime or legacy `codex/*` refs. -- CLI: `openclaw onboard --auth-choice openai-codex` or `openclaw models auth login --provider openai-codex` +- Plugin boundary: `openai/*` loads the OpenAI plugin; the native Codex app-server plugin is selected by the Codex harness runtime. +- CLI: `openclaw onboard --auth-choice openai` or `openclaw models auth login --provider openai` - Default transport is `auto` (WebSocket-first, SSE fallback) -- Override per OpenAI Codex model via `agents.defaults.models["openai-codex/"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`) +- Override per OpenAI Codex model via `agents.defaults.models["openai/"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`) - `params.serviceTier` is also forwarded on native Codex Responses requests (`chatgpt.com/backend-api`) - Hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`) are only attached on native Codex traffic to `chatgpt.com/backend-api`, not generic OpenAI-compatible proxies - Shares the same `/fast` toggle and `params.fastMode` config as direct `openai/*`; OpenClaw maps that to `service_tier=priority` -- `openai-codex/gpt-5.5` uses the Codex catalog native `contextWindow = 400000` and default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai-codex.models[].contextTokens` +- `openai/gpt-5.5` uses the Codex catalog native `contextWindow = 400000` and default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai.models[].contextTokens` - Policy note: OpenAI Codex OAuth is explicitly supported for external tools/workflows like OpenClaw. -- For the common subscription plus native Codex runtime route, sign in with `openai-codex` auth but configure `openai/gpt-5.5`; OpenAI agent turns select Codex by default. +- For the common subscription plus native Codex runtime route, sign in with `openai` auth and configure `openai/gpt-5.5`; OpenAI agent turns select Codex by default. - Use provider/model `agentRuntime.id: "openclaw"` only when you want the built-in OpenClaw route; otherwise keep `openai/gpt-5.5` on the default Codex harness. - `openai-codex/gpt-*` refs remain a legacy OpenAI Codex route. Prefer `openai/gpt-5.5` on the native Codex runtime for new agent config, and run `openclaw doctor --fix` when you want to migrate old `openai-codex/*` refs to canonical `openai/*` refs. @@ -166,7 +166,7 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope { models: { providers: { - "openai-codex": { + openai: { models: [{ id: "gpt-5.5", contextTokens: 160000 }], }, }, diff --git a/docs/concepts/models.md b/docs/concepts/models.md index 94eb4d354bed..f5058983657f 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -23,7 +23,7 @@ sidebarTitle: "Models CLI" -Model refs choose a provider and model. They do not usually choose the low-level agent runtime. OpenAI agent refs are the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Subscription Copilot refs (`github-copilot/*`) can additionally be opted into the external GitHub Copilot agent runtime plugin — that path stays explicit (no `auto` fallback). Explicit runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai-codex` auth profile. See [Agent runtimes](/concepts/agent-runtimes) and [GitHub Copilot agent runtime](/plugins/copilot). +Model refs choose a provider and model. They do not usually choose the low-level agent runtime. OpenAI agent refs are the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Subscription Copilot refs (`github-copilot/*`) can additionally be opted into the external GitHub Copilot agent runtime plugin — that path stays explicit (no `auto` fallback). Explicit runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai` OAuth profile. See [Agent runtimes](/concepts/agent-runtimes) and [GitHub Copilot agent runtime](/plugins/copilot). ## How model selection works @@ -150,7 +150,7 @@ If you want to limit providers without manually listing every model, add agents: { defaults: { models: { - "openai-codex/*": {}, + "openai/*": {}, "vllm/*": {}, }, }, diff --git a/docs/concepts/oauth.md b/docs/concepts/oauth.md index 8a3c261ce287..0733d09683e6 100644 --- a/docs/concepts/oauth.md +++ b/docs/concepts/oauth.md @@ -45,7 +45,7 @@ To reduce that, OpenClaw treats `auth-profiles.json` as a **token sink**: - the runtime reads credentials from **one place** - we can keep multiple profiles and route them deterministically - external CLI reuse is provider-specific: Codex CLI can bootstrap an empty - `openai-codex:default` profile, but once OpenClaw has a local OAuth profile, + `openai:default` profile, but once OpenClaw has a local OAuth profile, the local refresh token is canonical. If that local refresh token is rejected, OpenClaw can use a usable same-account Codex CLI token as a runtime-only fallback; other integrations can remain externally managed and re-read their @@ -131,7 +131,7 @@ Flow shape (PKCE): 5. exchange at `https://auth.openai.com/oauth/token` 6. extract `accountId` from the access token and store `{ access, refresh, expires, accountId }` -Wizard path is `openclaw onboard` → auth choice `openai-codex`. +Wizard path is `openclaw onboard` → auth choice `openai`. ## Refresh + expiry @@ -147,7 +147,7 @@ At runtime: - exception: some external CLI credentials stay externally managed; OpenClaw re-reads those CLI auth stores instead of spending copied refresh tokens. Codex CLI bootstrap is intentionally narrower: it seeds an empty - `openai-codex:default` profile, then OpenClaw-owned refreshes keep the local + `openai:default` profile, then OpenClaw-owned refreshes keep the local profile canonical. If the local Codex refresh fails and Codex CLI has a usable token for the same account, OpenClaw may use that token for the current runtime request without writing it back to `auth-profiles.json`. diff --git a/docs/gateway/authentication.md b/docs/gateway/authentication.md index b59347e293e3..8a49f28092c2 100644 --- a/docs/gateway/authentication.md +++ b/docs/gateway/authentication.md @@ -199,8 +199,8 @@ Use `openclaw models auth login --provider --profile-id ` for providers that support named auth profiles during login. ```bash -openclaw models auth login --provider openai-codex --profile-id openai-codex:ritsuko -openclaw models auth login --provider openai-codex --profile-id openai-codex:lain +openclaw models auth login --provider openai --profile-id openai:ritsuko +openclaw models auth login --provider openai --profile-id openai:lain ``` This is the easiest way to keep multiple OAuth logins for the same provider diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index 5991b237f8ff..8ecd9e8a1ff6 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -450,7 +450,7 @@ Time format in system prompt. Default: `auto` (OS preference). - `elevatedDefault`: default elevated-output level for agents. Values: `"off"`, `"on"`, `"ask"`, `"full"`. Default: `"on"`. - `model.primary`: format `provider/model` (e.g. `openai/gpt-5.5` for OpenAI API-key or Codex OAuth access). If you omit the provider, OpenClaw tries an alias first, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider (deprecated compatibility behavior, so prefer explicit `provider/model`). If that provider no longer exposes the configured default model, OpenClaw falls back to the first configured provider/model instead of surfacing a stale removed-provider default. - `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific, for example `temperature`, `maxTokens`, `cacheRetention`, `context1m`, `responsesServerCompaction`, `responsesCompactThreshold`, OpenRouter `provider` routing, `chat_template_kwargs`, `extra_body`/`extraBody`). - - Use `provider/*` entries such as `"openai-codex/*": {}` or `"vllm/*": {}` to show all discovered models for selected providers without manually listing every model id. + - Use `provider/*` entries such as `"openai/*": {}` or `"vllm/*": {}` to show all discovered models for selected providers without manually listing every model id. - Add `agentRuntime` to a `provider/*` entry when every dynamically discovered model for that provider should use the same runtime. Exact `provider/model` runtime policy still wins over the wildcard. - Safe edits: use `openclaw config set agents.defaults.models '' --strict-json --merge` to add entries. `config set` refuses replacements that would remove existing allowlist entries unless you pass `--replace`. - Provider-scoped configure/onboarding flows merge selected provider models into this map and preserve unrelated providers already configured. diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index 94cac8ccb935..1fa16a3aefae 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -82,12 +82,11 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number. "anthropic:default": { provider: "anthropic", mode: "api_key" }, "anthropic:work": { provider: "anthropic", mode: "api_key" }, "openai:default": { provider: "openai", mode: "api_key" }, - "openai-codex:personal": { provider: "openai-codex", mode: "oauth" }, + "openai:personal": { provider: "openai", mode: "oauth" }, }, order: { anthropic: ["anthropic:default", "anthropic:work"], - openai: ["openai:default"], - "openai-codex": ["openai-codex:personal"], + openai: ["openai:personal", "openai:default"], }, }, diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 2f10eac476c7..907f74d4eb4c 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -935,11 +935,11 @@ Notes: profiles: { "anthropic:default": { provider: "anthropic", mode: "api_key" }, "anthropic:work": { provider: "anthropic", mode: "api_key" }, - "openai-codex:personal": { provider: "openai-codex", mode: "oauth" }, + "openai:personal": { provider: "openai", mode: "oauth" }, }, order: { anthropic: ["anthropic:default", "anthropic:work"], - "openai-codex": ["openai-codex:personal"], + openai: ["openai:personal"], }, }, } diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index e1f369572817..397d1984ac8d 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -143,7 +143,7 @@ must be paired with `--lint`; regular doctor and repair runs reject them. - Talk config migration from legacy flat `talk.*` fields into `talk.provider` + `talk.providers.`. - Browser migration checks for legacy Chrome extension configs and Chrome MCP readiness. - OpenCode provider override warnings (`models.providers.opencode` / `models.providers.opencode-go`). - - Codex OAuth shadowing warnings (`models.providers.openai-codex`). + - Legacy OpenAI Codex provider/profile migration (`openai-codex` → `openai`) and shadowing warnings for stale `models.providers.openai-codex`. - OAuth TLS prerequisites check for OpenAI Codex OAuth profiles. - Plugin/tool allowlist warnings when `plugins.allow` is restrictive but tool policy still asks for wildcard or plugin-owned tools. - Legacy on-disk state migration (sessions/agent dir/WhatsApp auth). @@ -171,7 +171,7 @@ must be paired with `--lint`; regular doctor and repair runs reject them. - Channel status warnings (probed from the running gateway). - Channel-specific permission checks live under `openclaw channels capabilities`; for example, Discord voice channel permissions are audited with `openclaw channels capabilities --channel discord --target channel:`. - WhatsApp responsiveness checks for degraded Gateway event-loop health with local TUI clients still running; `--fix` stops only verified local TUI clients. - - Codex route repair for legacy `openai-codex/*` model refs in primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and session route pins; `--fix` rewrites them to `openai/*`, removes stale session/whole-agent runtime pins, and leaves canonical OpenAI agent refs on the default Codex harness. + - Codex route repair for legacy `openai-codex/*` model refs in primary models, fallbacks, image/video generation models, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and session route pins; `--fix` rewrites them to `openai/*`, migrates `openai-codex:*` auth profiles/order to `openai:*`, removes stale session/whole-agent runtime pins, and leaves canonical OpenAI agent refs on the default Codex harness. - Supervisor config audit (launchd/systemd/schtasks) with optional repair. - Embedded proxy environment cleanup for gateway services that captured shell `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` values during install or update. - Gateway runtime best-practice checks (Node vs Bun, version-manager paths). @@ -327,10 +327,10 @@ That stages grounded durable candidates into the short-term dreaming store while Doctor checks for legacy `openai-codex/*` model refs. Native Codex harness routing uses canonical `openai/*` model refs; OpenAI agent turns go through the Codex app-server harness instead of the OpenClaw OpenAI provider path. - In `--fix` / `--repair` mode, doctor rewrites affected default-agent and per-agent refs, including primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale persisted session route state: + In `--fix` / `--repair` mode, doctor rewrites affected default-agent and per-agent refs, including primary models, fallbacks, image/video generation models, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale persisted session route state: - `openai-codex/gpt-*` becomes `openai/gpt-*`. - - Codex intent moves to provider/model-scoped `agentRuntime.id: "codex"` entries for repaired agent model refs so `openai-codex:...` auth profiles can still be selected after the model ref becomes `openai/*`. + - Codex intent moves to provider/model-scoped `agentRuntime.id: "codex"` entries for repaired agent model refs. - Stale whole-agent runtime config and persisted session runtime pins are removed because runtime selection is provider/model-scoped. - Existing provider/model runtime policy is preserved unless the repaired legacy model ref needs Codex routing to keep the old auth path. - Existing model fallback lists are preserved with their legacy entries rewritten; copied per-model settings move from the legacy key to the canonical `openai/*` key. diff --git a/docs/gateway/logging.md b/docs/gateway/logging.md index d24795abb005..bc0ac8f08588 100644 --- a/docs/gateway/logging.md +++ b/docs/gateway/logging.md @@ -19,7 +19,7 @@ At startup, the Gateway logs the resolved default agent model together with the mode defaults that affect new sessions, for example: ```text -agent model: openai-codex/gpt-5.5 (thinking=medium, fast=on) +agent model: openai/gpt-5.5 (thinking=medium, fast=on) ``` `thinking` comes from the default agent, model params, or global agent default; diff --git a/docs/help/faq-first-run.md b/docs/help/faq-first-run.md index 7e15e0779db1..700a2143e588 100644 --- a/docs/help/faq-first-run.md +++ b/docs/help/faq-first-run.md @@ -599,22 +599,24 @@ and troubleshooting see the main [FAQ](/help/faq). native Codex app-server execution. `openai-codex/gpt-*` model refs are legacy config repaired by `openclaw doctor --fix`. Direct OpenAI API-key access remains available for non-agent OpenAI API surfaces and for agent - models through an ordered `openai-codex` API-key profile. + models through an ordered `openai` API-key profile. See [Model providers](/concepts/model-providers) and [Onboarding (CLI)](/start/wizard). - `openai-codex` is the provider and auth-profile id for ChatGPT/Codex OAuth. + `openai` is the provider and auth-profile id for both OpenAI API keys and + ChatGPT/Codex OAuth. You may still see `openai-codex` in legacy config and + migration warnings. Older configs also used it as a model prefix: - `openai/gpt-5.5` = ChatGPT/Codex subscription auth with native Codex runtime for agent turns - `openai-codex/gpt-5.5` = legacy model route repaired by `openclaw doctor --fix` - - `openai/gpt-5.5` plus an ordered `openai-codex` API-key profile = API-key auth for an OpenAI agent model - - `openai-codex:...` = auth profile id, not a model ref + - `openai/gpt-5.5` plus an ordered `openai` API-key profile = API-key auth for an OpenAI agent model + - `openai-codex:...` = legacy auth profile id migrated by `openclaw doctor --fix` If you want the direct OpenAI Platform billing/limit path, set `OPENAI_API_KEY`. If you want ChatGPT/Codex subscription auth, sign in with - `openclaw models auth login --provider openai-codex`. Keep the model ref as + `openclaw models auth login --provider openai`. Keep the model ref as `openai/gpt-5.5`; `openai-codex/*` model refs are legacy config that `openclaw doctor --fix` rewrites. diff --git a/docs/help/faq-models.md b/docs/help/faq-models.md index 4835bea9f722..e2dfcc31c51a 100644 --- a/docs/help/faq-models.md +++ b/docs/help/faq-models.md @@ -160,9 +160,9 @@ troubleshooting, see the main [FAQ](/help/faq). Yes. Treat model choice and runtime choice separately: - - **Native Codex coding agent:** set `agents.defaults.model.primary` to `openai/gpt-5.5`. Sign in with `openclaw models auth login --provider openai-codex` when you want ChatGPT/Codex subscription auth. + - **Native Codex coding agent:** set `agents.defaults.model.primary` to `openai/gpt-5.5`. Sign in with `openclaw models auth login --provider openai` when you want ChatGPT/Codex subscription auth. - **Direct OpenAI API tasks outside the agent loop:** configure `OPENAI_API_KEY` for images, embeddings, speech, realtime, and other non-agent OpenAI API surfaces. - - **OpenAI agent API-key auth:** use `/model openai/gpt-5.5` with an ordered `openai-codex` API-key profile. + - **OpenAI agent API-key auth:** use `/model openai/gpt-5.5` with an ordered `openai` API-key profile. - **Sub-agents:** route coding tasks to a Codex-focused agent with its own `openai/gpt-5.5` model. See [Models](/concepts/models) and [Slash commands](/tools/slash-commands). diff --git a/docs/help/testing-live.md b/docs/help/testing-live.md index 5043cc1e8350..6f8a3899d878 100644 --- a/docs/help/testing-live.md +++ b/docs/help/testing-live.md @@ -76,7 +76,7 @@ Live tests are split into two layers so we can isolate failures: - `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M2.7, Grok 4.3) - `OPENCLAW_LIVE_MODELS=small` to run the constrained small-model allowlist (Qwen 8B/9B local-compatible routes, OpenRouter Qwen/GLM, and Z.AI GLM) - `OPENCLAW_LIVE_MODELS=all` is an alias for the modern allowlist - - or `OPENCLAW_LIVE_MODELS="openai/gpt-5.5,openai-codex/gpt-5.5,anthropic/claude-opus-4-6,..."` (comma allowlist) + - or `OPENCLAW_LIVE_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,..."` (comma allowlist) - Modern/all and small sweeps default to their curated caps; set `OPENCLAW_LIVE_MAX_MODELS=0` for an exhaustive selected-profile sweep or a positive number for a smaller cap. - Exhaustive sweeps use `OPENCLAW_LIVE_TEST_TIMEOUT_MS` for the whole direct-model test timeout. Default: 60 minutes. - Direct-model probes run with 20-way parallelism by default; set `OPENCLAW_LIVE_MODEL_CONCURRENCY` to override. @@ -350,7 +350,7 @@ Narrow, explicit allowlists are fastest and least flaky: - `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` - Tool calling across several providers: - - `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,openai-codex/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` + - `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` - Google focus (Gemini API key + Antigravity): - Gemini (API key): `OPENCLAW_LIVE_GATEWAY_MODELS="google/gemini-3-flash-preview" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` @@ -378,7 +378,7 @@ There is no fixed "CI model list" (live is opt-in), but these are the **recommen This is the "common models" run we expect to keep working: - OpenAI (non-Codex): `openai/gpt-5.5` -- OpenAI Codex OAuth: `openai-codex/gpt-5.5` +- OpenAI ChatGPT/Codex OAuth: `openai/gpt-5.5` - Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-6`) - Google (Gemini API): `google/gemini-3.1-pro-preview` and `google/gemini-3-flash-preview` (avoid older Gemini 2.x models) - Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash` @@ -387,7 +387,7 @@ This is the "common models" run we expect to keep working: - MiniMax: `minimax/MiniMax-M2.7` Run gateway smoke with tools + image: -`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,openai-codex/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3.1-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` +`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3.1-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` ### Baseline: tool calling (Read + optional Exec) @@ -420,7 +420,7 @@ If you have keys enabled, we also support testing via: More providers you can include in the live matrix (if you have creds/config): -- Built-in: `openai`, `openai-codex`, `anthropic`, `google`, `google-vertex`, `google-antigravity`, `google-gemini-cli`, `zai`, `openrouter`, `opencode`, `opencode-go`, `xai`, `groq`, `cerebras`, `mistral`, `github-copilot` +- Built-in: `openai`, `anthropic`, `google`, `google-vertex`, `google-antigravity`, `google-gemini-cli`, `zai`, `openrouter`, `opencode`, `opencode-go`, `xai`, `groq`, `cerebras`, `mistral`, `github-copilot` - Via `models.providers` (custom endpoints): `minimax` (cloud/API), plus any OpenAI/Anthropic-compatible proxy (LM Studio, vLLM, LiteLLM, etc.) diff --git a/docs/nodes/media-understanding.md b/docs/nodes/media-understanding.md index feef5af3e26d..e478c76869ee 100644 --- a/docs/nodes/media-understanding.md +++ b/docs/nodes/media-understanding.md @@ -277,11 +277,11 @@ For CLI entries, **set `capabilities` explicitly** to avoid surprising matches. ## Provider support matrix (OpenClaw integrations) -| Capability | Provider integration | Notes | -| ---------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Image | OpenAI, OpenAI Codex OAuth, Codex app-server, OpenRouter, Anthropic, Google, MiniMax, Moonshot, Qwen, Z.AI, config providers | Vendor plugins register image support; `openai-codex/*` uses OAuth provider plumbing; `codex/*` uses a bounded Codex app-server turn; MiniMax and MiniMax OAuth both use `MiniMax-VL-01`; image-capable config providers auto-register. | -| Audio | OpenAI, Groq, xAI, Deepgram, OpenRouter, Google, SenseAudio, ElevenLabs, Mistral | Provider transcription (Whisper/Groq/xAI/Deepgram/OpenRouter STT/Gemini/SenseAudio/Scribe/Voxtral). | -| Video | Google, Qwen, Moonshot | Provider video understanding via vendor plugins; Qwen video understanding uses the Standard DashScope endpoints. | +| Capability | Provider integration | Notes | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Image | OpenAI, OpenAI Codex OAuth, Codex app-server, OpenRouter, Anthropic, Google, MiniMax, Moonshot, Qwen, Z.AI, config providers | Vendor plugins register image support; `openai/*` can use API-key or Codex OAuth routing; `codex/*` uses a bounded Codex app-server turn; MiniMax and MiniMax OAuth both use `MiniMax-VL-01`; image-capable config providers auto-register. | +| Audio | OpenAI, Groq, xAI, Deepgram, OpenRouter, Google, SenseAudio, ElevenLabs, Mistral | Provider transcription (Whisper/Groq/xAI/Deepgram/OpenRouter STT/Gemini/SenseAudio/Scribe/Voxtral). | +| Video | Google, Qwen, Moonshot | Provider video understanding via vendor plugins; Qwen video understanding uses the Standard DashScope endpoints. | **MiniMax note** diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index 6ddd1dd6b26d..37636690c079 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -44,7 +44,7 @@ Discord, Slack, or another channel remains the communication surface. - Codex app-server `0.125.0` or newer. The bundled plugin manages a compatible Codex app-server binary by default, so local `codex` commands on `PATH` do not affect normal harness startup. -- Codex auth available through `openclaw models auth login --provider openai-codex`, +- Codex auth available through `openclaw models auth login --provider openai`, an app-server account in the agent's Codex home, or an explicit Codex API-key auth profile. @@ -61,7 +61,7 @@ canonical `openai/gpt-*` model ref. Sign in with Codex OAuth: ```bash -openclaw models auth login --provider openai-codex +openclaw models auth login --provider openai ``` Enable the bundled `codex` plugin and select an OpenAI agent model: @@ -112,7 +112,7 @@ harness options in OpenClaw config, and use the CLI only for Codex auth: | Enable the harness | `plugins.entries.codex.enabled: true` | OpenClaw config | | Keep an allowlisted plugin install | Include `codex` in `plugins.allow` | OpenClaw config | | Route OpenAI agent turns through Codex | `agents.defaults.model` or `agents.list[].model` as `openai/gpt-*` | OpenClaw agent config | -| Sign in with Codex OAuth | `openclaw models auth login --provider openai-codex` | CLI auth profile | +| Sign in with ChatGPT/Codex OAuth | `openclaw models auth login --provider openai` | CLI auth profile | | Add API-key backup for Codex runs | `openai:*` API-key profile listed after subscription auth in `auth.order.openai` | CLI auth profile + OpenClaw config | | Fail closed when Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | OpenClaw model/provider config | | Use direct OpenAI API traffic | Provider or model `agentRuntime.id: "openclaw"` with normal OpenAI auth | OpenClaw model/provider config | @@ -153,7 +153,7 @@ instead of silently switching compaction backends. { auth: { order: { - openai: ["openai-codex:user@example.com", "openai:api-key-backup"], + openai: ["openai:user@example.com", "openai:api-key-backup"], }, }, } @@ -444,7 +444,8 @@ For upload mechanics and runtime-level diagnostics boundaries, see Auth is selected in this order: 1. Ordered OpenAI auth profiles for the agent, preferably under - `auth.order.openai`. Existing `openai-codex:*` profile ids remain valid. + `auth.order.openai`. Run `openclaw doctor --fix` to migrate older + `openai-codex:*` profile ids and `auth.order.openai-codex`. 2. The app-server's existing account in that agent's Codex home. 3. For local stdio app-server launches only, `CODEX_API_KEY`, then `OPENAI_API_KEY`, when no app-server account is present and OpenAI auth is @@ -720,7 +721,7 @@ Ask affected collaborators to run this read-only command on their OpenClaw host: Useful excerpts usually include `openai/gpt-5.5` or `openai/gpt-5.4`, `Runtime: OpenAI Codex`, `agentRuntime.id` or `harnessRuntime`, `candidateProvider: "openai"`, and a `401`, `Incorrect API key`, or -`No API key` result. A corrected run should show the `openai-codex` OAuth +`No API key` result. A corrected run should show the OpenAI OAuth path instead of a plain OpenAI API-key failure. **Legacy `openai-codex/*` config remains:** run `openclaw doctor --fix`. diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index d8e5ca8e9f78..fa7775bc5949 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -636,7 +636,7 @@ read without importing the plugin runtime. "realtimeTranscriptionProviders": ["openai"], "realtimeVoiceProviders": ["openai"], "memoryEmbeddingProviders": ["local"], - "mediaUnderstandingProviders": ["openai", "openai-codex"], + "mediaUnderstandingProviders": ["openai"], "imageGenerationProviders": ["openai"], "videoGenerationProviders": ["qwen"], "webFetchProviders": ["firecrawl"], diff --git a/docs/plugins/memory-lancedb.md b/docs/plugins/memory-lancedb.md index b48b378ea926..d81e739d3801 100644 --- a/docs/plugins/memory-lancedb.md +++ b/docs/plugins/memory-lancedb.md @@ -125,8 +125,8 @@ embeddings: } ``` -OpenAI Codex / ChatGPT OAuth (`openai-codex`) is not an OpenAI Platform -embeddings credential. For OpenAI embeddings, use an OpenAI API key auth profile, +OpenAI Codex / ChatGPT OAuth is not an OpenAI Platform embeddings credential. +For OpenAI embeddings, use an OpenAI API key auth profile, `OPENAI_API_KEY`, or `models.providers.openai.apiKey`. OAuth-only users can use another embedding-capable provider such as GitHub Copilot or Ollama. diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md index 9db8735fd7c4..c6f8b6ab5068 100644 --- a/docs/plugins/plugin-inventory.md +++ b/docs/plugins/plugin-inventory.md @@ -107,7 +107,7 @@ commands. | [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`
included in OpenClaw | plugin | | [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`
included in OpenClaw | providers: ollama, ollama-cloud; contracts: memoryEmbeddingProviders, webSearchProviders | | [open-prose](/plugins/reference/open-prose) | OpenProse VM skill pack with a /prose slash command. | `@openclaw/open-prose`
included in OpenClaw | skills | -| [openai](/plugins/reference/openai) | Adds OpenAI, OpenAI Codex model provider support to OpenClaw. | `@openclaw/openai-provider`
included in OpenClaw | providers: openai, openai-codex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders | +| [openai](/plugins/reference/openai) | Adds OpenAI model provider support to OpenClaw, including ChatGPT/Codex OAuth. | `@openclaw/openai-provider`
included in OpenClaw | providers: openai; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders | | [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`
included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders | | [opencode-go](/plugins/reference/opencode-go) | Adds OpenCode Go model provider support to OpenClaw. | `@openclaw/opencode-go-provider`
included in OpenClaw | providers: opencode-go; contracts: mediaUnderstandingProviders | | [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`
included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders | diff --git a/docs/plugins/reference.md b/docs/plugins/reference.md index f43e5f294e8d..1ea77f273a47 100644 --- a/docs/plugins/reference.md +++ b/docs/plugins/reference.md @@ -95,7 +95,7 @@ pnpm plugins:inventory:gen | [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`
included in OpenClaw | plugin | | [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`
included in OpenClaw | providers: ollama, ollama-cloud; contracts: memoryEmbeddingProviders, webSearchProviders | | [open-prose](/plugins/reference/open-prose) | OpenProse VM skill pack with a /prose slash command. | `@openclaw/open-prose`
included in OpenClaw | skills | -| [openai](/plugins/reference/openai) | Adds OpenAI, OpenAI Codex model provider support to OpenClaw. | `@openclaw/openai-provider`
included in OpenClaw | providers: openai, openai-codex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders | +| [openai](/plugins/reference/openai) | Adds OpenAI model provider support to OpenClaw, including ChatGPT/Codex OAuth. | `@openclaw/openai-provider`
included in OpenClaw | providers: openai; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders | | [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`
included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders | | [opencode-go](/plugins/reference/opencode-go) | Adds OpenCode Go model provider support to OpenClaw. | `@openclaw/opencode-go-provider`
included in OpenClaw | providers: opencode-go; contracts: mediaUnderstandingProviders | | [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`
included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders | diff --git a/docs/plugins/reference/openai.md b/docs/plugins/reference/openai.md index 0b3aeb74d7c7..e8e935cc816f 100644 --- a/docs/plugins/reference/openai.md +++ b/docs/plugins/reference/openai.md @@ -16,7 +16,7 @@ Adds OpenAI, OpenAI Codex model provider support to OpenClaw. ## Surface -providers: openai, openai-codex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders +providers: openai; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders ## Related docs diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index 7023bc77e8bf..c96f6f124f65 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -365,7 +365,7 @@ API key auth, and dynamic model resolution. | `kilocode-thinking` | Kilo reasoning wrapper on the shared proxy stream path, with `kilo/auto` and unsupported proxy reasoning ids skipping injected thinking | `kilocode` | | `moonshot-thinking` | Moonshot binary native-thinking payload mapping from config + `/think` level | `moonshot` | | `minimax-fast-mode` | MiniMax fast-mode model rewrite on the shared stream path | `minimax`, `minimax-portal` | - | `openai-responses-defaults` | Shared native OpenAI/Codex Responses wrappers: attribution headers, `/fast`/`serviceTier`, text verbosity, native Codex web search, reasoning-compat payload shaping, and Responses context management | `openai`, `openai-codex` | + | `openai-responses-defaults` | Shared native OpenAI/Codex Responses wrappers: attribution headers, `/fast`/`serviceTier`, text verbosity, native Codex web search, reasoning-compat payload shaping, and Responses context management | `openai` | | `openrouter-thinking` | OpenRouter reasoning wrapper for proxy routes, with unsupported-model/`auto` skips handled centrally | `openrouter` | | `tool-stream-default-on` | Default-on `tool_stream` wrapper for providers like Z.AI that want tool streaming unless explicitly disabled | `zai` | diff --git a/docs/providers/openai.md b/docs/providers/openai.md index 28bb89fe065e..0bc8516d5379 100644 --- a/docs/providers/openai.md +++ b/docs/providers/openai.md @@ -8,8 +8,8 @@ title: "OpenAI" --- OpenAI provides developer APIs for GPT models, and Codex is also available as a -ChatGPT-plan coding agent through OpenAI's Codex clients. OpenClaw keeps those -surfaces separate so config stays predictable. +ChatGPT-plan coding agent through OpenAI's Codex clients. OpenClaw uses one +provider id, `openai`, for both auth shapes. OpenClaw uses `openai/*` as the canonical OpenAI model route. Embedded agent turns on OpenAI models run through the native Codex app-server runtime by @@ -38,7 +38,7 @@ changing config. | Direct API-key billing for agent models | `openai/gpt-5.5` plus a Codex-compatible API-key profile | Use `auth.order.openai` to place the backup after subscription auth. | | Direct API-key billing through explicit OpenClaw | `openai/gpt-5.5` plus provider/model runtime `openclaw` | Select a normal `openai` API-key profile. | | Latest ChatGPT Instant API alias | `openai/chat-latest` | Direct API-key only. Moving alias for experiments, not the default. | -| ChatGPT/Codex subscription auth through OpenClaw | `openai/gpt-5.5` plus provider/model runtime `openclaw` | Select an `openai-codex` auth profile for the compatibility route. | +| ChatGPT/Codex subscription auth through OpenClaw | `openai/gpt-5.5` plus provider/model runtime `openclaw` | Select an `openai` OAuth profile for the compatibility route. | | Image generation or editing | `openai/gpt-image-2` | Works with either `OPENAI_API_KEY` or OpenAI Codex OAuth. | | Transparent-background images | `openai/gpt-image-1.5` | Use `outputFormat=png` or `webp` and `openai.background=transparent`. | @@ -46,20 +46,20 @@ changing config. The names are similar but not interchangeable: -| Name you see | Layer | Meaning | -| --------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `openai` | Provider prefix | Canonical OpenAI model route; agent turns use the Codex runtime. | -| `openai-codex` | Legacy auth/profile prefix | Older OpenAI Codex OAuth/subscription profile namespace. Existing profiles and `auth.order.openai-codex` still work. | -| `codex` plugin | Plugin | Bundled OpenClaw plugin that provides native Codex app-server runtime and `/codex` chat controls. | -| provider/model `agentRuntime.id: codex` | Agent runtime | Force the native Codex app-server harness for matching embedded turns. | -| `/codex ...` | Chat command set | Bind/control Codex app-server threads from a conversation. | -| `runtime: "acp", agentId: "codex"` | ACP session route | Explicit fallback path that runs Codex through ACP/acpx. | +| Name you see | Layer | Meaning | +| --------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------- | +| `openai` | Provider prefix | Canonical OpenAI model route; agent turns use the Codex runtime. | +| `openai-codex` | Legacy prefix | Older model/profile namespace. `openclaw doctor --fix` migrates it to `openai`. | +| `codex` plugin | Plugin | Bundled OpenClaw plugin that provides native Codex app-server runtime and `/codex` chat controls. | +| provider/model `agentRuntime.id: codex` | Agent runtime | Force the native Codex app-server harness for matching embedded turns. | +| `/codex ...` | Chat command set | Bind/control Codex app-server threads from a conversation. | +| `runtime: "acp", agentId: "codex"` | ACP session route | Explicit fallback path that runs Codex through ACP/acpx. | This means a config can intentionally contain `openai/*` model refs while auth -profiles still point at Codex-compatible credentials. Prefer `auth.order.openai` -for new config; existing `openai-codex:*` profiles and `auth.order.openai-codex` -remain supported. `openclaw doctor --fix` rewrites legacy `openai-codex/*` model -refs to the canonical OpenAI model route. +profiles point at either API-key or ChatGPT/Codex OAuth credentials. Use +`auth.order.openai` for config; `openclaw doctor --fix` rewrites legacy +`openai-codex/*` model refs, `openai-codex:*` profile ids, and +`auth.order.openai-codex` to the canonical OpenAI route. GPT-5.5 is available through both direct OpenAI Platform API-key access and @@ -72,7 +72,7 @@ direct API-key auth for an OpenAI agent model. OpenAI agent model turns require the bundled Codex app-server plugin. Explicit OpenClaw runtime config remains available as an opt-in compatibility route. When OpenClaw is -explicitly selected with an `openai-codex` auth profile, OpenClaw keeps the +explicitly selected with an `openai` OAuth profile, OpenClaw keeps the public model ref as `openai/*` and routes internally through the Codex-auth transport. Run `openclaw doctor --fix` to repair stale `openai-codex/*`, `codex-cli/*`, or old runtime session pins that do not come from @@ -84,7 +84,7 @@ explicit runtime config. | OpenAI capability | OpenClaw surface | Status | | ------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Chat / Responses | `openai/` model provider | Yes | -| Codex subscription models | `openai/` with `openai-codex` OAuth | Yes | +| Codex subscription models | `openai/` with OpenAI OAuth | Yes | | Legacy Codex model refs | `openai-codex/` or `codex-cli/` | Repaired by doctor to `openai/` | | Codex app-server harness | `openai/` with omitted runtime or provider/model `agentRuntime.id: codex` | Yes | | Server-side web search | Native OpenAI Responses tool | Yes, when web search is enabled and no provider pinned | @@ -101,7 +101,7 @@ explicit runtime config. Control UI Talk with `talk.realtime.provider: "openai"`) goes through the public **OpenAI Platform Realtime API**, which is billed against OpenAI Platform credits rather than Codex/ChatGPT subscription quota. An account - with healthy Codex OAuth that runs `openai-codex/*` chat models without + with healthy OpenAI OAuth that runs Codex-backed chat models without issue can still hit `insufficient_quota` / "You exceeded your current quota" on the first Realtime turn if the same OpenAI organization has no Platform billing set up. @@ -111,10 +111,10 @@ Fix: top up Platform credits at for the organization backing your realtime credentials. Realtime accepts either a Platform `OPENAI_API_KEY` (configured via `talk.realtime.providers.openai.apiKey` for Control UI Talk, or `plugins.entries.voice-call.config.realtime.providers.openai.apiKey` -for Voice Call) or an `openai-codex` OAuth profile whose underlying +for Voice Call) or an `openai` OAuth profile whose underlying organization has Platform billing — both routes mint Realtime client secrets through the Platform API, so either way the org needs funded Platform -credits. For chat turns you can still use `openai-codex/*` against the same +credits. For chat turns you can still use Codex-backed `openai/*` models against the same OpenClaw install; Realtime is the one route that needs Platform billing. @@ -178,14 +178,14 @@ Choose your preferred auth method and follow the setup steps. | ---------------------- | -------------------------- | --------------------------- | ---------------- | | `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Codex-compatible OpenAI profile | | `openai/gpt-5.4-mini` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Codex-compatible OpenAI profile | - | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime | `openai` profile or selected `openai-codex` profile | + | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime | Selected `openai` profile | `openai/*` agent models use the Codex app-server harness. To use API-key auth for an agent model, create a Codex-compatible API-key profile and order it with `auth.order.openai`; `OPENAI_API_KEY` remains the direct fallback for - non-agent OpenAI API surfaces. Older `auth.order.openai-codex` entries still - work. + non-agent OpenAI API surfaces. Run `openclaw doctor --fix` to migrate older + `auth.order.openai-codex` entries. ### Config example @@ -215,7 +215,7 @@ Choose your preferred auth method and follow the setup steps. model. - OpenClaw does **not** expose `openai/gpt-5.3-codex-spark`. Live OpenAI API requests reject that direct provider route. Use `openai-codex/gpt-5.3-codex-spark` only when the Codex catalog exposes it for your signed-in account. + OpenClaw does **not** expose `gpt-5.3-codex-spark` on the direct OpenAI API-key route. It is available only through Codex subscription catalog entries when your signed-in account exposes it. @@ -226,19 +226,19 @@ Choose your preferred auth method and follow the setup steps. ```bash - openclaw onboard --auth-choice openai-codex + openclaw onboard --auth-choice openai ``` Or run OAuth directly: ```bash - openclaw models auth login --provider openai-codex + openclaw models auth login --provider openai ``` For headless or callback-hostile setups, add `--device-code` to sign in with a ChatGPT device-code flow instead of the localhost browser callback: ```bash - openclaw models auth login --provider openai-codex --device-code + openclaw models auth login --provider openai --device-code ``` @@ -252,7 +252,7 @@ Choose your preferred auth method and follow the setup steps. ```bash - openclaw models list --provider openai-codex + openclaw models list --provider openai ``` After the gateway is running, send `/codex status` or `/codex models` @@ -265,16 +265,16 @@ Choose your preferred auth method and follow the setup steps. | Model ref | Runtime config | Route | Auth | |-----------|----------------|-------|------| | `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in or ordered `openai` auth profile | - | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime with internal Codex-auth transport | Selected `openai-codex` profile | - | `openai-codex/gpt-5.5` | repaired by doctor | Legacy route rewritten to `openai/gpt-5.5` | Existing `openai-codex` profile | + | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime with internal Codex-auth transport | Selected `openai` OAuth profile | + | `openai-codex/gpt-5.5` | repaired by doctor | Legacy route rewritten to `openai/gpt-5.5` | Migrated OpenAI OAuth profile | | `codex-cli/gpt-5.5` | repaired by doctor | Legacy CLI route rewritten to `openai/gpt-5.5` | Codex app-server auth | Prefer `openai/gpt-5.5` for new subscription-backed agent config. Older `openai-codex/gpt-*` refs are legacy OpenClaw routes, not the native Codex runtime path; run `openclaw doctor --fix` when you want to migrate them to canonical - `openai/*` refs. `openai-codex/gpt-5.3-codex-spark` is the exception for - accounts whose Codex catalog advertises that model; direct `openai/*` and + `openai/*` refs. `gpt-5.3-codex-spark` remains limited to accounts whose + Codex subscription catalog advertises that model; direct OpenAI API-key and Azure refs for it remain suppressed. @@ -282,8 +282,8 @@ Choose your preferred auth method and follow the setup steps. The `openai-codex/*` model prefix is legacy config repaired by doctor. For the common subscription plus native runtime setup, sign in with Codex auth but keep the model ref as `openai/gpt-5.5`. New config should put OpenAI - agent auth order under `auth.order.openai`; older `auth.order.openai-codex` - entries remain valid. + agent auth order under `auth.order.openai`; doctor migrates older + `auth.order.openai-codex` entries. ### Config example @@ -314,7 +314,7 @@ Choose your preferred auth method and follow the setup steps. auth: { order: { openai: [ - "openai-codex:user@example.com", + "openai:user@example.com", "openai:api-key-backup", ], }, @@ -333,7 +333,7 @@ Choose your preferred auth method and follow the setup steps. ```bash openclaw models status - openclaw models auth list --provider openai-codex + openclaw models auth list --provider openai openclaw config get agents.defaults.model --json openclaw config get models.providers.openai.agentRuntime --json ``` @@ -342,7 +342,7 @@ Choose your preferred auth method and follow the setup steps. ```bash openclaw models status --agent - openclaw models auth list --agent --provider openai-codex + openclaw models auth list --agent --provider openai ``` If an older config still has `openai-codex/gpt-*` or a stale OpenAI runtime @@ -353,25 +353,25 @@ Choose your preferred auth method and follow the setup steps. openclaw config validate ``` - If `models auth list --provider openai-codex` shows no usable profile, sign + If `models auth list --provider openai` shows no usable profile, sign in again: ```bash - openclaw models auth login --provider openai-codex - openclaw models status --probe --probe-provider openai-codex + openclaw models auth login --provider openai + openclaw models status --probe --probe-provider openai ``` Use `--profile-id` when you want multiple Codex OAuth logins in the same agent and later want to control them via auth ordering or `/model ...@`: ```bash - openclaw models auth login --provider openai-codex --profile-id openai-codex:ritsuko - openclaw models auth login --provider openai-codex --profile-id openai-codex:lain + openclaw models auth login --provider openai --profile-id openai:ritsuko + openclaw models auth login --provider openai --profile-id openai:lain ``` - `openai/*` is the model route for OpenAI agent turns through Codex. The - `openai-codex` auth/profile provider id remains accepted for existing - profiles and CLI listing. + `openai/*` is the model route for OpenAI agent turns through Codex. Run + `openclaw doctor --fix` to migrate older `openai-codex` profile ids and + order entries before relying on profile ordering. ### Status indicator @@ -401,7 +401,7 @@ Choose your preferred auth method and follow the setup steps. { models: { providers: { - "openai-codex": { + openai: { models: [{ id: "gpt-5.5", contextTokens: 160000 }], }, }, @@ -431,8 +431,8 @@ runtime config or provider/model `agentRuntime.id: "codex"`, but its auth is still account-based. OpenClaw selects auth in this order: 1. Ordered OpenAI auth profiles for the agent, preferably under - `auth.order.openai`. Existing `openai-codex:*` profiles and - `auth.order.openai-codex` remain valid for older installs. + `auth.order.openai`. Run `openclaw doctor --fix` to migrate older + `openai-codex:*` profiles and `auth.order.openai-codex`. 2. The app-server's existing account, such as a local Codex CLI ChatGPT sign-in. 3. For local stdio app-server launches only, `CODEX_API_KEY`, then `OPENAI_API_KEY`, when the app-server reports no account and still requires @@ -509,8 +509,8 @@ Use the same `--output-format` and `--background` flags with `openclaw infer image edit` when starting from an input file. `--openai-background` remains available as an OpenAI-specific alias. -For Codex OAuth installs, keep the same `openai/gpt-image-2` ref. When an -`openai-codex` OAuth profile is configured, OpenClaw resolves that stored OAuth +For ChatGPT/Codex OAuth installs, keep the same `openai/gpt-image-2` ref. When an +`openai` OAuth profile is configured, OpenClaw resolves that stored OAuth access token and sends image requests through the Codex Responses backend. It does not first try `OPENAI_API_KEY` or silently fall back to an API key for that request. Configure `models.providers.openai` explicitly with an API key, @@ -697,10 +697,10 @@ Legacy `plugins.entries.openai.config.personality` is still read as a compatibil | Prompt | `...openai.prompt` | (unset) | | Silence duration | `...openai.silenceDurationMs` | `800` | | VAD threshold | `...openai.vadThreshold` | `0.5` | - | Auth | `...openai.apiKey`, `OPENAI_API_KEY`, or `openai-codex` OAuth | API keys connect directly; OAuth mints a Realtime transcription client secret | + | Auth | `...openai.apiKey`, `OPENAI_API_KEY`, or `openai` OAuth | API keys connect directly; OAuth mints a Realtime transcription client secret | - Uses a WebSocket connection to `wss://api.openai.com/v1/realtime` with G.711 u-law (`g711_ulaw` / `audio/pcmu`) audio. When only `openai-codex` OAuth is configured, the Gateway mints an ephemeral Realtime transcription client secret before opening the WebSocket. This streaming provider is for Voice Call's realtime transcription path; Discord voice currently records short segments and uses the batch `tools.media.audio` transcription path instead. + Uses a WebSocket connection to `wss://api.openai.com/v1/realtime` with G.711 u-law (`g711_ulaw` / `audio/pcmu`) audio. When only `openai` OAuth is configured, the Gateway mints an ephemeral Realtime transcription client secret before opening the WebSocket. This streaming provider is for Voice Call's realtime transcription path; Discord voice currently records short segments and uses the batch `tools.media.audio` transcription path instead.
@@ -717,7 +717,7 @@ Legacy `plugins.entries.openai.config.personality` is still read as a compatibil | Silence duration | `...openai.silenceDurationMs` | `500` | | Prefix padding | `...openai.prefixPaddingMs` | `300` | | Reasoning effort | `...openai.reasoningEffort` | (unset) | - | Auth | `...openai.apiKey`, `OPENAI_API_KEY`, or `openai-codex` OAuth | Browser Talk and non-Azure backend bridges can use Codex OAuth | + | Auth | `...openai.apiKey`, `OPENAI_API_KEY`, or `openai` OAuth | Browser Talk and non-Azure backend bridges can use OpenAI OAuth | Available built-in Realtime voices for `gpt-realtime-2`: `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`, `marin`, `cedar`. @@ -740,7 +740,7 @@ Legacy `plugins.entries.openai.config.personality` is still read as a compatibil Control UI Talk uses OpenAI browser realtime sessions with a Gateway-minted ephemeral client secret and a direct browser WebRTC SDP exchange against the OpenAI Realtime API. When no direct OpenAI API key is configured, the - Gateway can mint that client secret with the selected `openai-codex` OAuth + Gateway can mint that client secret with the selected `openai` OAuth profile. Gateway relay and Voice Call backend realtime WebSocket bridges use the same OAuth fallback for native OpenAI endpoints. Maintainer live verification is available with diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index f09809a49482..9b614bbcdf26 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -557,9 +557,9 @@ Two ways to start an ACP session: Explicit model override for the ACP child session. Codex ACP spawns - normalize OpenClaw Codex refs such as `openai-codex/gpt-5.4` to Codex - ACP startup config before `session/new`; slash forms such as - `openai-codex/gpt-5.4/high` also set Codex ACP reasoning effort. + normalize OpenAI refs such as `openai/gpt-5.4` to Codex ACP startup + config before `session/new`; slash forms such as `openai/gpt-5.4/high` + also set Codex ACP reasoning effort. Other harnesses must advertise ACP `models` and support `session/set_model`; otherwise OpenClaw/acpx fails clearly instead of silently falling back to the target agent default. @@ -792,7 +792,7 @@ operations: | Command | Maps to | Notes | | ---------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/acp model ` | runtime config key `model` | For Codex ACP, OpenClaw normalizes `openai-codex/` to the adapter model id and maps slash reasoning suffixes such as `openai-codex/gpt-5.4/high` to `reasoning_effort`. | +| `/acp model ` | runtime config key `model` | For Codex ACP, OpenClaw normalizes `openai/` to the adapter model id and maps slash reasoning suffixes such as `openai/gpt-5.4/high` to `reasoning_effort`. | | `/acp set thinking ` | canonical option `thinking` | OpenClaw sends the backend-advertised equivalent when present, preferring `thinking`, then `effort`, `reasoning_effort`, or `thought_level`. For Codex ACP, the adapter maps values to `reasoning_effort`. | | `/acp permissions ` | canonical option `permissionProfile` | OpenClaw sends the backend-advertised equivalent when present, such as `approval_policy`, `permission_profile`, `permissions`, or `permission_mode`. | | `/acp timeout ` | canonical option `timeoutSeconds` | OpenClaw sends the backend-advertised equivalent when present, such as `timeout` or `timeout_seconds`. | diff --git a/docs/tools/image-generation.md b/docs/tools/image-generation.md index 0bdfe5a9637c..1d2c5fcec2dc 100644 --- a/docs/tools/image-generation.md +++ b/docs/tools/image-generation.md @@ -22,7 +22,7 @@ idempotent direct fallback with only the missing images. The tool only appears when at least one image-generation provider is available. If you do not see `image_generate` in your agent's tools, configure `agents.defaults.imageGenerationModel`, set up a provider API key, -or sign in with OpenAI Codex OAuth. +or sign in with OpenAI ChatGPT/Codex OAuth. ## Quick start @@ -46,9 +46,9 @@ or sign in with OpenAI Codex OAuth. } ``` - Codex OAuth uses the same `openai/gpt-image-2` model ref. When an - `openai-codex` OAuth profile is configured, OpenClaw routes image - requests through that OAuth profile instead of first trying + ChatGPT/Codex OAuth uses the same `openai/gpt-image-2` model ref. When an + `openai` OAuth profile is configured, OpenClaw routes image requests + through that OAuth profile instead of first trying `OPENAI_API_KEY`. Explicit `models.providers.openai` config (API key, custom/Azure base URL) opts back into the direct OpenAI Images API route. @@ -77,7 +77,7 @@ internal image endpoints remain blocked by default. | Goal | Model ref | Auth | | ---------------------------------------------------- | -------------------------------------------------- | -------------------------------------- | | OpenAI image generation with API billing | `openai/gpt-image-2` | `OPENAI_API_KEY` | -| OpenAI image generation with Codex subscription auth | `openai/gpt-image-2` | OpenAI Codex OAuth | +| OpenAI image generation with Codex subscription auth | `openai/gpt-image-2` | OpenAI ChatGPT/Codex OAuth | | OpenAI transparent-background PNG/WebP | `openai/gpt-image-1.5` | `OPENAI_API_KEY` or OpenAI Codex OAuth | | DeepInfra image generation | `deepinfra/black-forest-labs/FLUX-1-schnell` | `DEEPINFRA_API_KEY` | | fal Krea 2 expressive/style-directed generation | `fal/krea/v2/medium/text-to-image` | `FAL_KEY` | @@ -105,7 +105,7 @@ backend emits it. | Google | `gemini-3.1-flash-image-preview` | Yes | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | | LiteLLM | `gpt-image-2` | Yes (up to 5 input images) | `LITELLM_API_KEY` | | MiniMax | `image-01` | Yes (subject reference) | `MINIMAX_API_KEY` or MiniMax OAuth (`minimax-portal`) | -| OpenAI | `gpt-image-2` | Yes (up to 4 images) | `OPENAI_API_KEY` or OpenAI Codex OAuth | +| OpenAI | `gpt-image-2` | Yes (up to 4 images) | `OPENAI_API_KEY` or OpenAI ChatGPT/Codex OAuth | | OpenRouter | `google/gemini-3.1-flash-image-preview` | Yes (up to 5 input images) | `OPENROUTER_API_KEY` | | Vydra | `grok-imagine` | No | `VYDRA_API_KEY` | | xAI | `grok-imagine-image` | Yes (up to 5 images) | `XAI_API_KEY` | @@ -280,7 +280,7 @@ to 10 for GPT Image 2 edits, up to 10 style references for Krea 2, and up to OpenAI image generation defaults to `openai/gpt-image-2`. If an - `openai-codex` OAuth profile is configured, OpenClaw reuses the same + `openai` OAuth profile is configured, OpenClaw reuses the same OAuth profile used by Codex subscription chat models and sends the image request through the Codex Responses backend. Legacy Codex base URLs such as `https://chatgpt.com/backend-api` are canonicalized to diff --git a/docs/tools/llm-task.md b/docs/tools/llm-task.md index 4dfc6ad7a71c..9b3bad15d9bc 100644 --- a/docs/tools/llm-task.md +++ b/docs/tools/llm-task.md @@ -47,10 +47,10 @@ Use `tools.allow` only when you want restrictive allowlist mode. "llm-task": { "enabled": true, "config": { - "defaultProvider": "openai-codex", + "defaultProvider": "openai", "defaultModel": "gpt-5.5", "defaultAuthProfileId": "main", - "allowedModels": ["openai/gpt-5.4"], + "allowedModels": ["openai/gpt-5.5"], "maxTokens": 800, "timeoutMs": 30000 } diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index c0f23c0dd27f..621bc836c7f4 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -186,7 +186,7 @@ Key policy rules: surfaces, such as a provider/model ref, channel config, CLI backend, or agent harness runtime. - OpenAI-family Codex routing keeps provider and runtime plugin boundaries - separate: `openai-codex/*` is legacy OpenAI-provider config, while the bundled + separate: `openai-codex/*` is legacy config repaired by doctor, while the bundled `codex` plugin owns Codex app-server runtime for canonical `openai/*` agent refs, explicit `agentRuntime.id: "codex"`, and legacy `codex/*` refs. diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 47a436d45aef..784bd21e2dbd 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -70,7 +70,7 @@ title: "Thinking levels" 4. Per-model config: `agents.defaults.models["/"].params.fastMode` 5. Fallback: `off` - For `openai/*`, fast mode maps to OpenAI priority processing by sending `service_tier=priority` on supported Responses requests. -- For `openai-codex/*`, fast mode sends the same `service_tier=priority` flag on Codex Responses. OpenClaw keeps one shared `/fast` toggle across both auth paths. +- For Codex-backed `openai/*` models, fast mode sends the same `service_tier=priority` flag on Codex Responses. OpenClaw keeps one shared `/fast` toggle across both auth paths. - For direct public `anthropic/*` requests, including OAuth-authenticated traffic sent to `api.anthropic.com`, fast mode maps to Anthropic service tiers: `/fast on` sets `service_tier=auto`, `/fast off` sets `service_tier=standard_only`. - For `minimax/*` on the Anthropic-compatible path, `/fast on` (or `params.fastMode: true`) rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`. - Explicit Anthropic `serviceTier` / `service_tier` model params override the fast-mode default when both are set. OpenClaw still skips Anthropic service-tier injection for non-Anthropic proxy base URLs. diff --git a/docs/tools/web.md b/docs/tools/web.md index 2bb6dc7a2115..330ebef375b4 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -123,7 +123,7 @@ Direct OpenAI Responses models use OpenAI's hosted `web_search` tool automatical Codex-capable models can optionally use the provider-native Responses `web_search` tool instead of OpenClaw's managed `web_search` function. - Configure it under `tools.web.search.openaiCodex` -- It only activates for Codex-capable models (`openai-codex/*` or providers using `api: "openai-codex-responses"`) +- It only activates for Codex-capable OpenAI models (`openai/*` models using `api: "openai-codex-responses"`) - Managed `web_search` still applies to non-Codex models - `mode: "cached"` is the default and recommended setting - `tools.web.search.enabled: false` disables both managed and native search diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 58c91b102ac6..485352c7faa0 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -183,7 +183,7 @@ Activity entries keep only sanitized summaries and redacted, truncated output pr - Talk mode uses a registered realtime voice provider. Configure OpenAI with `talk.realtime.provider: "openai"` plus either `talk.realtime.providers.openai.apiKey`, `OPENAI_API_KEY`, or an `openai-codex` OAuth profile; configure Google with `talk.realtime.provider: "google"` plus `talk.realtime.providers.google.apiKey`. For hosted GPT realtime models, OpenClaw prefers the `openai-codex` OAuth profile before `OPENAI_API_KEY`; an explicit OpenAI realtime `apiKey` remains the advanced override. The browser never receives a standard provider API key. OpenAI receives an ephemeral Realtime client secret for WebRTC. Google Live receives a one-use constrained Live API auth token for a browser WebSocket session, with instructions and tool declarations locked into the token by the Gateway. Providers that only expose a backend realtime bridge run through the Gateway relay transport, so credentials and vendor sockets stay server-side while browser audio moves through authenticated Gateway RPCs. The Realtime session prompt is assembled by the Gateway; `talk.client.create` does not accept caller-provided instruction overrides. + Talk mode uses a registered realtime voice provider. Configure OpenAI with `talk.realtime.provider: "openai"` plus either `talk.realtime.providers.openai.apiKey`, `OPENAI_API_KEY`, or an `openai` OAuth profile; configure Google with `talk.realtime.provider: "google"` plus `talk.realtime.providers.google.apiKey`. For hosted GPT realtime models, OpenClaw prefers the `openai` OAuth profile before `OPENAI_API_KEY`; an explicit OpenAI realtime `apiKey` remains the advanced override. The browser never receives a standard provider API key. OpenAI receives an ephemeral Realtime client secret for WebRTC. Google Live receives a one-use constrained Live API auth token for a browser WebSocket session, with instructions and tool declarations locked into the token by the Gateway. Providers that only expose a backend realtime bridge run through the Gateway relay transport, so credentials and vendor sockets stay server-side while browser audio moves through authenticated Gateway RPCs. The Realtime session prompt is assembled by the Gateway; `talk.client.create` does not accept caller-provided instruction overrides. The Chat composer includes a Talk options button next to the Talk start/stop button. The options apply to the next Talk session and can override provider, transport, model, voice, reasoning effort, VAD threshold, silence duration, and prefix padding. When an option is blank, the Gateway uses configured defaults where available or the provider default. Selecting Gateway relay forces the backend relay path; selecting WebRTC keeps the session client-owned and fails instead of silently falling back to relay if the provider cannot create a browser session. diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index 9e9e36499dad..5595c3e93d02 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -26,9 +26,9 @@ import type { } from "./protocol.js"; import { resolveCodexAppServerSpawnEnv } from "./transport-stdio.js"; -const CODEX_APP_SERVER_AUTH_PROVIDER = "openai-codex"; +const CODEX_APP_SERVER_AUTH_PROVIDER = "openai"; const OPENAI_PROVIDER = "openai"; -const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default"; +const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai:default"; const CODEX_HOME_ENV_VAR = "CODEX_HOME"; const HOME_ENV_VAR = "HOME"; const CODEX_APP_SERVER_HOME_DIRNAME = "codex-home"; diff --git a/extensions/codex/src/app-server/local-runtime-attribution.ts b/extensions/codex/src/app-server/local-runtime-attribution.ts index 92251fce303a..2784b06619d9 100644 --- a/extensions/codex/src/app-server/local-runtime-attribution.ts +++ b/extensions/codex/src/app-server/local-runtime-attribution.ts @@ -2,7 +2,6 @@ import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness const OPENAI_PROVIDER_ID = "openai"; const OPENAI_RESPONSES_API = "openai-responses"; -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; const OPENAI_CODEX_RESPONSES_API = "openai-codex-responses"; export type CodexLocalRuntimeAttribution = { @@ -27,7 +26,7 @@ export function resolveCodexLocalRuntimeAttribution( normalizeRuntimeId(params.model.api) === OPENAI_RESPONSES_API ) { return { - provider: OPENAI_CODEX_PROVIDER_ID, + provider: OPENAI_PROVIDER_ID, api: OPENAI_CODEX_RESPONSES_API, }; } diff --git a/extensions/codex/src/app-server/session-binding.ts b/extensions/codex/src/app-server/session-binding.ts index c41075e595f7..fff6ea64deeb 100644 --- a/extensions/codex/src/app-server/session-binding.ts +++ b/extensions/codex/src/app-server/session-binding.ts @@ -15,7 +15,7 @@ import { import type { PluginAppPolicyContext } from "./plugin-thread-config.js"; import type { CodexServiceTier } from "./protocol.js"; -const CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER = "openai-codex"; +const CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER = "openai"; const PUBLIC_OPENAI_MODEL_PROVIDER = "openai"; type ProviderAuthAliasLookupParams = Parameters[1]; diff --git a/extensions/codex/src/command-account.ts b/extensions/codex/src/command-account.ts index e0ffda8a0db5..01e0277d07e8 100644 --- a/extensions/codex/src/command-account.ts +++ b/extensions/codex/src/command-account.ts @@ -21,7 +21,7 @@ import { import type { CodexControlRequestOptions, SafeValue } from "./command-rpc.js"; const OPENAI_PROVIDER_ID = "openai"; -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const OPENAI_CODEX_PROVIDER_ID = OPENAI_PROVIDER_ID; type AuthProfileOrderConfig = Parameters[0]["cfg"]; diff --git a/extensions/codex/src/migration/auth.ts b/extensions/codex/src/migration/auth.ts index 877cafd6cab4..2bd7c4138f69 100644 --- a/extensions/codex/src/migration/auth.ts +++ b/extensions/codex/src/migration/auth.ts @@ -28,7 +28,6 @@ import { readJsonObject } from "./helpers.js"; import type { CodexSource } from "./source.js"; import type { resolveCodexMigrationTargets } from "./targets.js"; -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; const OPENAI_PROVIDER_ID = "openai"; const OPENAI_CODEX_DEFAULT_MODEL = "openai/gpt-5.5"; const CODEX_IMPORT_DISPLAY_NAME = "Codex import"; @@ -48,7 +47,7 @@ type AgentDefaultModelConfigEntry = AgentDefaultModelConfigs[string]; type CodexAuthCredential = | { kind: "oauth"; - provider: typeof OPENAI_CODEX_PROVIDER_ID; + provider: typeof OPENAI_PROVIDER_ID; profileId: string; result: ProviderAuthResult; modelConfigs: AgentDefaultModelConfigs; @@ -123,7 +122,7 @@ async function buildCodexOAuthCredential(source: CodexSource): Promise; const result = buildOauthProviderAuthResult({ - providerId: OPENAI_CODEX_PROVIDER_ID, + providerId: OPENAI_PROVIDER_ID, defaultModel: OPENAI_CODEX_DEFAULT_MODEL, access: credential.access, refresh: credential.refresh, @@ -142,7 +141,7 @@ async function buildCodexOAuthCredential(source: CodexSource): Promise { workspaceDir: fixture.workspaceDir, }), ); - expectRecordFields(findItem(skippedPlan.items, "auth:openai-codex"), { + expectRecordFields(findItem(skippedPlan.items, "auth:openai"), { kind: "auth", status: "skipped", sensitive: true, @@ -363,7 +363,7 @@ describe("buildCodexMigrationProvider", () => { includeSecrets: true, }); const plan = await provider.plan(ctx); - expectRecordFields(findItem(plan.items, "auth:openai-codex"), { + expectRecordFields(findItem(plan.items, "auth:openai"), { kind: "auth", status: "planned", sensitive: true, @@ -371,7 +371,7 @@ describe("buildCodexMigrationProvider", () => { const result = await provider.apply(ctx, plan); - expectRecordFields(findItem(result.items, "auth:openai-codex"), { status: "migrated" }); + expectRecordFields(findItem(result.items, "auth:openai"), { status: "migrated" }); const authStore = JSON.parse( await fs.readFile( path.join(fixture.stateDir, "agents", "main", "agent", "auth-profiles.json"), @@ -383,17 +383,17 @@ describe("buildCodexMigrationProvider", () => { { access?: string; provider?: string; refresh?: string; type?: string } >; }; - expect(authStore.profiles?.["openai-codex:account-acct_test"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_test"]).toEqual( expect.objectContaining({ type: "oauth", - provider: "openai-codex", + provider: "openai", access: accessToken, refresh: "refresh-test-token", }), ); - expect(configState.auth?.profiles?.["openai-codex:account-acct_test"]).toEqual( + expect(configState.auth?.profiles?.["openai:account-acct_test"]).toEqual( expect.objectContaining({ - provider: "openai-codex", + provider: "openai", mode: "oauth", }), ); @@ -434,8 +434,8 @@ describe("buildCodexMigrationProvider", () => { }, auth: { profiles: { - "openai-codex:account-acct_conflict": { - provider: "openai-codex", + "openai:account-acct_conflict": { + provider: "openai", mode: "api_key", }, }, @@ -453,12 +453,12 @@ describe("buildCodexMigrationProvider", () => { }), ); - expect(findItem(plan.items, "auth:openai-codex")).toEqual( + expect(findItem(plan.items, "auth:openai")).toEqual( expect.objectContaining({ status: "conflict", reason: "auth profile exists", details: expect.objectContaining({ - profileId: "openai-codex:account-acct_conflict", + profileId: "openai:account-acct_conflict", }), }), ); @@ -559,10 +559,10 @@ describe("buildCodexMigrationProvider", () => { includeSecrets: true, }); const plan = await provider.plan(ctx); - expect(findItem(plan.items, "auth:openai-codex").details).toEqual( + expect(findItem(plan.items, "auth:openai").details).toEqual( expect.objectContaining({ - profileId: "openai-codex:account-acct_planned", - sourceProfileId: "openai-codex:account-acct_planned", + profileId: "openai:account-acct_planned", + sourceProfileId: "openai:account-acct_planned", }), ); await writeFile( @@ -579,7 +579,7 @@ describe("buildCodexMigrationProvider", () => { const result = await provider.apply(ctx, plan); - expect(findItem(result.items, "auth:openai-codex")).toEqual( + expect(findItem(result.items, "auth:openai")).toEqual( expect.objectContaining({ status: "skipped", reason: "auth credential no longer present", @@ -619,9 +619,9 @@ describe("buildCodexMigrationProvider", () => { path.join(fixture.stateDir, "agents", "main", "agent", "auth-profiles.json"), JSON.stringify({ profiles: { - "openai-codex:account-acct_old": { + "openai:account-acct_old": { type: "oauth", - provider: "openai-codex", + provider: "openai", access: "old-access-token", refresh: "old-refresh-token", accountId: "acct_old", @@ -649,18 +649,18 @@ describe("buildCodexMigrationProvider", () => { }); const plan = await provider.plan(ctx); - expectRecordFields(findItem(plan.items, "auth:openai-codex"), { + expectRecordFields(findItem(plan.items, "auth:openai"), { status: "planned", }); - expect(findItem(plan.items, "auth:openai-codex").details).toEqual( + expect(findItem(plan.items, "auth:openai").details).toEqual( expect.objectContaining({ - profileId: "openai-codex:account-acct_new", + profileId: "openai:account-acct_new", }), ); const result = await provider.apply(ctx, plan); - expectRecordFields(findItem(result.items, "auth:openai-codex"), { status: "migrated" }); + expectRecordFields(findItem(result.items, "auth:openai"), { status: "migrated" }); const authStore = JSON.parse( await fs.readFile( path.join(fixture.stateDir, "agents", "main", "agent", "auth-profiles.json"), @@ -669,14 +669,14 @@ describe("buildCodexMigrationProvider", () => { ) as { profiles?: Record; }; - expect(authStore.profiles?.["openai-codex:account-acct_old"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_old"]).toEqual( expect.objectContaining({ access: "old-access-token", accountId: "acct_old", email: sharedEmail, }), ); - expect(authStore.profiles?.["openai-codex:account-acct_new"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_new"]).toEqual( expect.objectContaining({ access: accessToken, accountId: "acct_new", @@ -728,8 +728,8 @@ describe("buildCodexMigrationProvider", () => { const result = await provider.apply(ctx, plan); - expectRecordFields(findItem(result.items, "auth:openai-codex"), { status: "migrated" }); - expect(findItem(result.items, "auth:openai-codex").details).toEqual( + expectRecordFields(findItem(result.items, "auth:openai"), { status: "migrated" }); + expect(findItem(result.items, "auth:openai").details).toEqual( expect.objectContaining({ configUpdated: false, }), @@ -742,10 +742,10 @@ describe("buildCodexMigrationProvider", () => { ) as { profiles?: Record; }; - expect(authStore.profiles?.["openai-codex:account-acct_test"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_test"]).toEqual( expect.objectContaining({ type: "oauth", - provider: "openai-codex", + provider: "openai", access: accessToken, }), ); @@ -799,13 +799,13 @@ describe("buildCodexMigrationProvider", () => { const result = await provider.apply(ctx, plan); - expect(findItem(result.items, "auth:openai-codex").details).toEqual( + expect(findItem(result.items, "auth:openai").details).toEqual( expect.objectContaining({ configUpdated: false, configPatchReturned: true, }), ); - expect(findItem(result.items, "auth:openai-codex:config:auth")).toEqual( + expect(findItem(result.items, "auth:openai:config:auth")).toEqual( expect.objectContaining({ kind: "config", action: "merge", @@ -814,8 +814,8 @@ describe("buildCodexMigrationProvider", () => { path: ["auth"], value: expect.objectContaining({ profiles: expect.objectContaining({ - "openai-codex:account-acct_test": expect.objectContaining({ - provider: "openai-codex", + "openai:account-acct_test": expect.objectContaining({ + provider: "openai", mode: "oauth", }), }), @@ -823,7 +823,7 @@ describe("buildCodexMigrationProvider", () => { }), }), ); - expect(findItem(result.items, "auth:openai-codex:config:agents-defaults")).toEqual( + expect(findItem(result.items, "auth:openai:config:agents-defaults")).toEqual( expect.objectContaining({ kind: "config", action: "merge", @@ -1156,7 +1156,7 @@ describe("buildCodexMigrationProvider", () => { }, auth: { order: { - "openai-codex": ["openai-codex:target"], + openai: ["openai:target"], }, }, } as MigrationProviderContext["config"], diff --git a/extensions/llm-task/README.md b/extensions/llm-task/README.md index 738208f3d606..131dc36ef184 100644 --- a/extensions/llm-task/README.md +++ b/extensions/llm-task/README.md @@ -44,10 +44,10 @@ Designed to be called from workflow engines (for example, Lobster via "llm-task": { "enabled": true, "config": { - "defaultProvider": "openai-codex", - "defaultModel": "gpt-5.2", + "defaultProvider": "openai", + "defaultModel": "gpt-5.5", "defaultAuthProfileId": "main", - "allowedModels": ["openai-codex/gpt-5.2"], + "allowedModels": ["openai/gpt-5.5"], "maxTokens": 800, "timeoutMs": 30000 } diff --git a/extensions/llm-task/index.ts b/extensions/llm-task/index.ts index e124971f87ab..895af781a8b4 100644 --- a/extensions/llm-task/index.ts +++ b/extensions/llm-task/index.ts @@ -15,7 +15,7 @@ export default defineToolPlugin({ defaultAuthProfileId: Type.Optional(Type.String()), allowedModels: Type.Optional( Type.Array(Type.String(), { - description: "Allowlist of provider/model keys like openai-codex/gpt-5.2.", + description: "Allowlist of provider/model keys like openai/gpt-5.5.", }), ), maxTokens: optionalPositiveIntegerSchema(), diff --git a/extensions/llm-task/src/llm-task-tool.test.ts b/extensions/llm-task/src/llm-task-tool.test.ts index 6858ab4514b0..62670931ae0b 100644 --- a/extensions/llm-task/src/llm-task-tool.test.ts +++ b/extensions/llm-task/src/llm-task-tool.test.ts @@ -50,13 +50,13 @@ function fakeApi(overrides: any = {}) { name: "llm-task", source: "test", config: { - agents: { defaults: { workspace: "/tmp", model: { primary: "openai-codex/gpt-5.2" } } }, + agents: { defaults: { workspace: "/tmp", model: { primary: "openai/gpt-5.5" } } }, }, pluginConfig: {}, runtime: { version: "test", agent: { - defaults: { provider: "openai-codex", model: "gpt-5.2" }, + defaults: { provider: "openai", model: "gpt-5.5" }, runEmbeddedAgent, resolveThinkingPolicy, normalizeThinkingLevel, @@ -157,7 +157,7 @@ describe("llm-task tool (json-only)", () => { }), ).resolves.toEqual({ content: [{ type: "text", text: '{\n "foo": "bar"\n}' }], - details: { json: { foo: "bar" }, provider: "openai-codex", model: "gpt-5.2" }, + details: { json: { foo: "bar" }, provider: "openai", model: "gpt-5.5" }, }); await expect( @@ -173,7 +173,7 @@ describe("llm-task tool (json-only)", () => { }), ).resolves.toEqual({ content: [{ type: "text", text: '{\n "count": 1\n}' }], - details: { json: { count: 1 }, provider: "openai-codex", model: "gpt-5.2" }, + details: { json: { count: 1 }, provider: "openai", model: "gpt-5.5" }, }); }); @@ -248,8 +248,8 @@ describe("llm-task tool (json-only)", () => { const call = await executeEmbeddedRun({ prompt: "x", thinking: "high" }); expect(call.thinkLevel).toBe("high"); expect(resolveThinkingPolicy).toHaveBeenCalledWith({ - provider: "openai-codex", - model: "gpt-5.2", + provider: "openai", + model: "gpt-5.5", }); }); @@ -283,7 +283,7 @@ describe("llm-task tool (json-only)", () => { it("enforces allowedModels", async () => { mockEmbeddedRunJson({ ok: true }); const tool = createLlmTaskTool( - fakeApi({ pluginConfig: { allowedModels: ["openai-codex/gpt-5.2"] } }), + fakeApi({ pluginConfig: { allowedModels: ["openai/gpt-5.5"] } }), ); await expect( tool.execute("id", { prompt: "x", provider: "anthropic", model: "claude-4-sonnet" }), diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 1079f3663d62..0a61e4f53d56 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -126,7 +126,7 @@ export const llmTaskToolDefinition = { Type.Unknown({ description: "Optional JSON Schema to validate the returned JSON." }), ), provider: Type.Optional( - Type.String({ description: "Provider override (e.g. openai-codex, anthropic)." }), + Type.String({ description: "Provider override (e.g. openai, anthropic)." }), ), model: Type.Optional(Type.String({ description: "Model id override." })), thinking: Type.Optional(Type.String({ description: "Thinking level override." })), diff --git a/extensions/migrate-hermes/auth.ts b/extensions/migrate-hermes/auth.ts index 846d2e49d6bc..945a4fad4372 100644 --- a/extensions/migrate-hermes/auth.ts +++ b/extensions/migrate-hermes/auth.ts @@ -37,8 +37,9 @@ import { import type { HermesSource } from "./source.js"; import type { PlannedTargets } from "./targets.js"; -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; -const OPENAI_CODEX_DEFAULT_MODEL = "openai/gpt-5.5"; +const HERMES_OPENAI_CODEX_SOURCE_PROVIDER_ID = "openai-codex"; +const OPENAI_PROVIDER_ID = "openai"; +const OPENAI_DEFAULT_MODEL = "openai/gpt-5.5"; const HERMES_AUTH_DISPLAY_NAME = "Hermes import"; type AgentDefaultModelConfigs = NonNullable< @@ -91,8 +92,8 @@ function readProviderTokens( sourcePath: string, ): HermesCodexAuthCandidate | undefined { const providers = isRecord(auth.providers) ? auth.providers : {}; - const provider = isRecord(providers[OPENAI_CODEX_PROVIDER_ID]) - ? providers[OPENAI_CODEX_PROVIDER_ID] + const provider = isRecord(providers[HERMES_OPENAI_CODEX_SOURCE_PROVIDER_ID]) + ? providers[HERMES_OPENAI_CODEX_SOURCE_PROVIDER_ID] : undefined; const tokens = isRecord(provider?.tokens) ? provider.tokens : undefined; const access = readString(tokens?.access_token); @@ -115,8 +116,8 @@ function readPoolTokens( sourcePath: string, ): HermesCodexAuthCandidate[] { const pool = isRecord(auth.credential_pool) ? auth.credential_pool : {}; - const entries = Array.isArray(pool[OPENAI_CODEX_PROVIDER_ID]) - ? pool[OPENAI_CODEX_PROVIDER_ID] + const entries = Array.isArray(pool[HERMES_OPENAI_CODEX_SOURCE_PROVIDER_ID]) + ? pool[HERMES_OPENAI_CODEX_SOURCE_PROVIDER_ID] : []; const candidates: HermesCodexAuthCandidate[] = []; for (const entry of entries) { @@ -211,8 +212,8 @@ function buildAuthResult( accountId: candidate.accountId, }); return buildOauthProviderAuthResult({ - providerId: OPENAI_CODEX_PROVIDER_ID, - defaultModel: OPENAI_CODEX_DEFAULT_MODEL, + providerId: OPENAI_PROVIDER_ID, + defaultModel: OPENAI_DEFAULT_MODEL, access: candidate.access, refresh: candidate.refresh, expires: resolveOpenAICodexAccessTokenExpiry(candidate.access), @@ -228,7 +229,7 @@ function readProviderAuthModelConfigs(result: ProviderAuthResult): AgentDefaultM if (isRecord(models)) { return { ...models }; } - const defaultModel = readString(result.defaultModel) ?? OPENAI_CODEX_DEFAULT_MODEL; + const defaultModel = readString(result.defaultModel) ?? OPENAI_DEFAULT_MODEL; return { [defaultModel]: {} }; } @@ -420,8 +421,8 @@ export async function buildAuthItems(params: { ((targetExists && !matchedProfileId && !params.ctx.overwrite) || configConflict) && !skipped; const itemId = profiles.length === 1 - ? `auth:${OPENAI_CODEX_PROVIDER_ID}` - : `auth:${OPENAI_CODEX_PROVIDER_ID}:${profile.sourceProfileId}`; + ? `auth:${OPENAI_PROVIDER_ID}` + : `auth:${OPENAI_PROVIDER_ID}:${profile.sourceProfileId}`; return createMigrationItem({ id: itemId, kind: "auth", @@ -439,7 +440,7 @@ export async function buildAuthItems(params: { ? "OpenAI Codex OAuth credentials detected in Hermes." : "Import Hermes OpenAI Codex OAuth credentials and configure OpenAI Codex models.", details: { - provider: OPENAI_CODEX_PROVIDER_ID, + provider: OPENAI_PROVIDER_ID, profileId, ...(typeof profile.candidate.sourceCredentialIndex === "number" ? { sourceCredentialIndex: profile.candidate.sourceCredentialIndex } diff --git a/extensions/migrate-hermes/provider.secret-failure.test.ts b/extensions/migrate-hermes/provider.secret-failure.test.ts index d46ab21ce4df..56a94f749f41 100644 --- a/extensions/migrate-hermes/provider.secret-failure.test.ts +++ b/extensions/migrate-hermes/provider.secret-failure.test.ts @@ -152,18 +152,18 @@ describe("Hermes migration provider secret write failures", () => { expect(result.items).toEqual([ expect.objectContaining({ - id: "auth:openai-codex", + id: "auth:openai", kind: "auth", action: "create", source: path.join(source, "auth.json"), - target: `${path.join(stateDir, "agents", "main", "agent")}/auth-profiles.json#openai-codex:account-acct_fail`, + target: `${path.join(stateDir, "agents", "main", "agent")}/auth-profiles.json#openai:account-acct_fail`, status: "error", sensitive: true, reason: HERMES_REASON_AUTH_PROFILE_WRITE_FAILED, details: expect.objectContaining({ - provider: "openai-codex", - profileId: "openai-codex:account-acct_fail", - sourceProfileId: "openai-codex:account-acct_fail", + provider: "openai", + profileId: "openai:account-acct_fail", + sourceProfileId: "openai:account-acct_fail", }), }), ]); diff --git a/extensions/migrate-hermes/secrets.test.ts b/extensions/migrate-hermes/secrets.test.ts index 22d4bd8ece1f..3a369c16bc1e 100644 --- a/extensions/migrate-hermes/secrets.test.ts +++ b/extensions/migrate-hermes/secrets.test.ts @@ -406,7 +406,7 @@ describe("Hermes migration secret items", () => { expect(plan.items).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "auth:openai-codex", + id: "auth:openai", kind: "auth", status: "planned", sensitive: true, @@ -426,18 +426,18 @@ describe("Hermes migration secret items", () => { { access?: string; provider?: string; refresh?: string; type?: string } >; }; - const profile = authStore.profiles?.["openai-codex:account-acct_test"]; + const profile = authStore.profiles?.["openai:account-acct_test"]; expect(profile).toEqual( expect.objectContaining({ type: "oauth", - provider: "openai-codex", + provider: "openai", access: accessToken, refresh: "refresh-test-token", }), ); - expect(config.auth?.profiles?.["openai-codex:account-acct_test"]).toEqual( + expect(config.auth?.profiles?.["openai:account-acct_test"]).toEqual( expect.objectContaining({ - provider: "openai-codex", + provider: "openai", mode: "oauth", }), ); @@ -873,12 +873,12 @@ describe("Hermes migration secret items", () => { expect(plan.items).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "auth:openai-codex", + id: "auth:openai", kind: "auth", status: "planned", source: path.join(root, ".local", "share", "opencode", "auth.json"), details: expect.objectContaining({ - provider: "openai-codex", + provider: "openai", sourceKind: "opencode-auth-json", sourceLabel: "OpenCode OpenAI OAuth credential", }), @@ -897,10 +897,10 @@ describe("Hermes migration secret items", () => { { access?: string; accountId?: string; provider?: string; refresh?: string; type?: string } >; }; - expect(authStore.profiles?.["openai-codex:account-acct_opencode"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_opencode"]).toEqual( expect.objectContaining({ type: "oauth", - provider: "openai-codex", + provider: "openai", accountId: "acct_opencode", access: accessToken, refresh: "openai-refresh-token", @@ -965,24 +965,24 @@ describe("Hermes migration secret items", () => { .filter((item) => item.kind === "auth") .map((item) => ({ id: item.id, status: item.status })), ).toEqual([ - { id: "auth:openai-codex:openai-codex:hermes-import-1", status: "migrated" }, - { id: "auth:openai-codex:openai-codex:hermes-import-2", status: "migrated" }, + { id: "auth:openai:openai:hermes-import-1", status: "migrated" }, + { id: "auth:openai:openai:hermes-import-2", status: "migrated" }, ]); const authStore = JSON.parse( await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf8"), ) as { profiles?: Record; }; - expect(authStore.profiles?.["openai-codex:hermes-import-1"]).toEqual( + expect(authStore.profiles?.["openai:hermes-import-1"]).toEqual( expect.objectContaining({ - provider: "openai-codex", + provider: "openai", access: "opaque-hermes-access", refresh: "opaque-hermes-refresh", }), ); - expect(authStore.profiles?.["openai-codex:hermes-import-2"]).toEqual( + expect(authStore.profiles?.["openai:hermes-import-2"]).toEqual( expect.objectContaining({ - provider: "openai-codex", + provider: "openai", access: "opaque-opencode-access", refresh: "opaque-opencode-refresh", }), @@ -1021,7 +1021,7 @@ describe("Hermes migration secret items", () => { expect(plan.items).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "auth:openai-codex", + id: "auth:openai", details: expect.objectContaining({ sourceCredentialFingerprint: expect.any(String), sourceCredentialIndex: 0, @@ -1043,7 +1043,7 @@ describe("Hermes migration secret items", () => { ); const result = await provider.apply(ctx, plan); - const authItem = result.items.find((item) => item.id === "auth:openai-codex"); + const authItem = result.items.find((item) => item.id === "auth:openai"); expect(authItem).toEqual( expect.objectContaining({ @@ -1075,8 +1075,8 @@ describe("Hermes migration secret items", () => { }, auth: { profiles: { - "openai-codex:account-acct_conflict": { - provider: "openai-codex", + "openai:account-acct_conflict": { + provider: "openai", mode: "api_key", }, }, @@ -1107,14 +1107,14 @@ describe("Hermes migration secret items", () => { includeSecrets: true, }), ); - const authItem = plan.items.find((item) => item.id === "auth:openai-codex"); + const authItem = plan.items.find((item) => item.id === "auth:openai"); expect(authItem).toEqual( expect.objectContaining({ status: "conflict", reason: HERMES_REASON_AUTH_PROFILE_EXISTS, details: expect.objectContaining({ - profileId: "openai-codex:account-acct_conflict", + profileId: "openai:account-acct_conflict", }), }), ); @@ -1208,9 +1208,9 @@ describe("Hermes migration secret items", () => { .map((item) => item.details?.profileId) .toSorted((left, right) => String(left).localeCompare(String(right))), ).toEqual([ - "openai-codex:account-acct_active", - "openai-codex:account-acct_pool", - "openai-codex:account-acct_second_pool", + "openai:account-acct_active", + "openai:account-acct_pool", + "openai:account-acct_second_pool", ]); const result = await provider.apply(ctx, plan); @@ -1224,26 +1224,26 @@ describe("Hermes migration secret items", () => { { access?: string; provider?: string; refresh?: string; type?: string } >; }; - expect(authStore.profiles?.["openai-codex:account-acct_active"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_active"]).toEqual( expect.objectContaining({ type: "oauth", - provider: "openai-codex", + provider: "openai", access: activeAccessToken, refresh: "refresh-active-token", }), ); - expect(authStore.profiles?.["openai-codex:account-acct_pool"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_pool"]).toEqual( expect.objectContaining({ type: "oauth", - provider: "openai-codex", + provider: "openai", access: poolAccessToken, refresh: "refresh-pool-token", }), ); - expect(authStore.profiles?.["openai-codex:account-acct_second_pool"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_second_pool"]).toEqual( expect.objectContaining({ type: "oauth", - provider: "openai-codex", + provider: "openai", access: secondPoolAccessToken, refresh: "refresh-second-pool-token", }), @@ -1251,9 +1251,9 @@ describe("Hermes migration secret items", () => { expect( Object.keys(config.auth?.profiles ?? {}).toSorted((left, right) => left.localeCompare(right)), ).toEqual([ - "openai-codex:account-acct_active", - "openai-codex:account-acct_pool", - "openai-codex:account-acct_second_pool", + "openai:account-acct_active", + "openai:account-acct_pool", + "openai:account-acct_second_pool", ]); expect(config.agents?.defaults?.models?.["openai/gpt-5.5"]).toEqual({}); }); @@ -1299,9 +1299,9 @@ describe("Hermes migration secret items", () => { path.join(agentDir, "auth-profiles.json"), JSON.stringify({ profiles: { - "openai-codex:account-acct_old": { + "openai:account-acct_old": { type: "oauth", - provider: "openai-codex", + provider: "openai", access: "old-access-token", refresh: "old-refresh-token", accountId: "acct_old", @@ -1322,13 +1322,13 @@ describe("Hermes migration secret items", () => { runtime: makeConfigRuntime(config), }); const plan = await provider.plan(ctx); - const authItem = plan.items.find((item) => item.id === "auth:openai-codex"); + const authItem = plan.items.find((item) => item.id === "auth:openai"); expect(authItem).toEqual( expect.objectContaining({ status: "planned", details: expect.objectContaining({ - profileId: "openai-codex:account-acct_new", + profileId: "openai:account-acct_new", }), }), ); @@ -1341,14 +1341,14 @@ describe("Hermes migration secret items", () => { ) as { profiles?: Record; }; - expect(authStore.profiles?.["openai-codex:account-acct_old"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_old"]).toEqual( expect.objectContaining({ access: "old-access-token", accountId: "acct_old", email: sharedEmail, }), ); - expect(authStore.profiles?.["openai-codex:account-acct_new"]).toEqual( + expect(authStore.profiles?.["openai:account-acct_new"]).toEqual( expect.objectContaining({ access: accessToken, accountId: "acct_new", diff --git a/extensions/openai/api.ts b/extensions/openai/api.ts index ad6753b9d5f1..de2f5a21cbbb 100644 --- a/extensions/openai/api.ts +++ b/extensions/openai/api.ts @@ -12,7 +12,6 @@ export { export { buildOpenAICodexProvider } from "./openai-codex-catalog.js"; export { loginOpenAICodexOAuth } from "./openai-codex-oauth.runtime.js"; export { refreshOpenAICodexToken } from "./openai-codex-provider.runtime.js"; -export { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js"; -export { buildOpenAIProvider } from "./openai-provider.js"; +export { buildOpenAICodexProviderPlugin, buildOpenAIProvider } from "./openai-provider.js"; export { buildOpenAIRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js"; export { buildOpenAIRealtimeVoiceProvider } from "./realtime-voice-provider.js"; diff --git a/extensions/openai/image-generation-provider.test.ts b/extensions/openai/image-generation-provider.test.ts index 41fc8f13c6e0..2e959e6a4054 100644 --- a/extensions/openai/image-generation-provider.test.ts +++ b/extensions/openai/image-generation-provider.test.ts @@ -33,12 +33,18 @@ const { postJsonRequestMock: vi.fn(), postMultipartRequestMock: vi.fn(), assertOkOrThrowHttpErrorMock: vi.fn(async () => {}), - resolveProviderHttpRequestConfigMock: vi.fn((params) => ({ - baseUrl: params.baseUrl ?? params.defaultBaseUrl, - allowPrivateNetwork: Boolean(params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork), - headers: new Headers(params.defaultHeaders), - dispatcherPolicy: undefined, - })), + resolveProviderHttpRequestConfigMock: vi.fn((params) => { + const headers = new Headers(params.defaultHeaders); + new Headers(params.headers).forEach((value, key) => headers.set(key, value)); + return { + baseUrl: params.baseUrl ?? params.defaultBaseUrl, + allowPrivateNetwork: Boolean( + params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork, + ), + headers, + dispatcherPolicy: undefined, + }; + }), sanitizeConfiguredModelProviderRequestMock: vi.fn((request) => request), logInfoMock: vi.fn(), })); @@ -208,6 +214,26 @@ function createMixedCodexAuthStore() { }; } +function createMixedOpenAIAuthStore() { + return { + version: 1 as const, + profiles: { + "openai:chatgpt": { + type: "oauth" as const, + provider: "openai", + access: "codex-access", + refresh: "codex-refresh", + expires: Date.now() + 60_000, + }, + "openai:default": { + type: "api_key" as const, + provider: "openai", + key: "openai-key", + }, + }, + }; +} + type MockWithCalls = { mock: { calls: readonly (readonly unknown[])[]; @@ -237,6 +263,16 @@ type RequestCall = { }; type AuthResolutionCall = { + cfg?: { + models?: { + providers?: { + openai?: { + auth?: string; + }; + }; + }; + }; + credentialPrecedence?: string; provider?: string; store?: unknown; }; @@ -304,7 +340,6 @@ describe("openai image generation provider", () => { const provider = buildOpenAIImageGenerationProvider(); expect(provider.defaultModel).toBe("gpt-image-2"); - expect(provider.aliases).toContain("openai-codex"); expect(provider.models).toEqual([ "gpt-image-2", "gpt-image-1.5", @@ -375,6 +410,42 @@ describe("openai image generation provider", () => { ).toBe(false); }); + it("does not report OpenAI OAuth image auth as configured for custom OpenAI endpoints", () => { + const provider = buildOpenAIImageGenerationProvider(); + vi.stubEnv("OPENAI_API_KEY", ""); + isProviderApiKeyConfiguredMock.mockImplementation((params?: { provider?: string }) => { + return params?.provider === "openai"; + }); + ensureAuthProfileStoreMock.mockReturnValue({ + version: 1, + profiles: { + "openai:chatgpt": { + type: "oauth", + provider: "openai", + access: "chatgpt-access", + refresh: "chatgpt-refresh", + expires: Date.now() + 60_000, + }, + }, + }); + + expect( + provider.isConfigured?.({ + agentDir: "/tmp/agent", + cfg: { + models: { + providers: { + openai: { + baseUrl: "https://openai-compatible.example.test/v1", + models: [], + }, + }, + }, + }, + }), + ).toBe(false); + }); + it("does not report Codex OAuth image auth as configured for non-exact public OpenAI URLs", () => { const provider = buildOpenAIImageGenerationProvider(); @@ -854,7 +925,7 @@ describe("openai image generation provider", () => { Authorization: "Bearer codex-key", Accept: "text/event-stream", }); - expect(configCall.provider).toBe("openai-codex"); + expect(configCall.provider).toBe("openai"); expect(configCall.api).toBe("openai-codex-responses"); expect(configCall.capability).toBe("image"); const request = jsonRequestCall(); @@ -879,7 +950,7 @@ describe("openai image generation provider", () => { expect(body.tool_choice).toEqual({ type: "image_generation" }); expect(postMultipartRequestMock).not.toHaveBeenCalled(); expect(logInfoMock).toHaveBeenCalledWith( - "image auth selected: provider=openai-codex mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.5 timeoutMs=180000", + "image auth selected: provider=openai mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.5 timeoutMs=180000", ); expect(result.images).toEqual([ { @@ -1031,7 +1102,7 @@ describe("openai image generation provider", () => { expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1); expect(authResolutionCall().provider).toBe("openai-codex"); expect(httpConfigCall().defaultBaseUrl).toBe("https://chatgpt.com/backend-api/codex"); - expect(httpConfigCall().provider).toBe("openai-codex"); + expect(httpConfigCall().provider).toBe("openai"); expect(httpConfigCall().api).toBe("openai-codex-responses"); expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses"); expect(postMultipartRequestMock).not.toHaveBeenCalled(); @@ -1158,11 +1229,53 @@ describe("openai image generation provider", () => { ).toBe(false); expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses"); expect(logInfoMock).toHaveBeenCalledWith( - "image auth selected: provider=openai-codex mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.5 timeoutMs=180000", + "image auth selected: provider=openai mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.5 timeoutMs=180000", ); expect(result.images[0]?.buffer).toEqual(Buffer.from("codex-image")); }); + it("keeps explicit OpenAI API-key image config on native image requests", async () => { + resolveApiKeyForProviderMock.mockImplementation(async (params?: AuthResolutionCall) => { + if (params?.cfg?.models?.providers?.openai?.auth === "api-key") { + return { apiKey: "configured-openai-key", source: "models.json", mode: "api-key" }; + } + return { apiKey: "chatgpt-oauth-token", source: "profile:openai:chatgpt", mode: "oauth" }; + }); + mockGeneratedPngResponse(); + + const provider = buildOpenAIImageGenerationProvider(); + const authStore = createMixedOpenAIAuthStore(); + await provider.generateImage({ + provider: "openai", + model: "gpt-image-2", + prompt: "Draw using configured OpenAI image auth", + cfg: { + models: { + providers: { + openai: { + apiKey: "sk-configured", + baseUrl: "https://api.openai.com/v1", + models: [], + }, + }, + }, + }, + authStore, + }); + + expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1); + expect(authResolutionCall().provider).toBe("openai"); + expect(authResolutionCall().store).toBe(authStore); + expect(authResolutionCall().credentialPrecedence).toBe("env-first"); + expect(authResolutionCall().cfg?.models?.providers?.openai?.auth).toBe("api-key"); + expect(httpConfigCall().defaultBaseUrl).toBe("https://api.openai.com/v1"); + expect(httpConfigCall().defaultHeaders).toEqual({ + Authorization: "Bearer configured-openai-key", + }); + expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations"); + expect(logInfoMock).not.toHaveBeenCalled(); + }); + it("does not fall back to Codex OAuth for custom OpenAI-compatible image endpoints", async () => { mockCodexAuthOnly(); mockCodexImageStream({ imageData: "codex-image" }); @@ -1269,7 +1382,7 @@ describe("openai image generation provider", () => { }); expect(logInfoMock).toHaveBeenCalledWith( - "image auth selected: provider=openai-codex mode=oauth fakeignored transport=codex-responses requestedModel=gpt-image-2 forged=true next responsesModel=gpt-5.5 timeoutMs=180000", + "image auth selected: provider=openai mode=oauth fakeignored transport=codex-responses requestedModel=gpt-image-2 forged=true next responsesModel=gpt-5.5 timeoutMs=180000", ); }); @@ -1372,7 +1485,7 @@ describe("openai image generation provider", () => { }); expect(httpConfigCall().baseUrl).toBe("https://chatgpt.com/backend-api/codex"); - expect(httpConfigCall().provider).toBe("openai-codex"); + expect(httpConfigCall().provider).toBe("openai"); expect(httpConfigCall().api).toBe("openai-codex-responses"); expect(httpConfigCall().capability).toBe("image"); expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses"); @@ -1421,6 +1534,88 @@ describe("openai image generation provider", () => { expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations"); }); + it("keeps explicit OpenAI request settings on native image requests", async () => { + mockGeneratedPngResponse(); + resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => { + if (params?.provider === "openai") { + return { apiKey: "openai-key", source: "models.json", mode: "api-key" }; + } + if (params?.provider === "openai-codex") { + return { apiKey: "codex-key", source: "profile:openai-codex:default", mode: "oauth" }; + } + return {}; + }); + + const provider = buildOpenAIImageGenerationProvider(); + await provider.generateImage({ + provider: "openai", + model: "gpt-image-2", + prompt: "Draw using explicit request settings", + cfg: { + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + api: "openai-responses", + headers: { "X-Test-OpenAI": "direct" }, + request: { allowPrivateNetwork: true }, + models: [], + }, + }, + }, + }, + authStore: createCodexOAuthAuthStore(), + }); + + expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1); + expect(authResolutionCall().provider).toBe("openai"); + expect(httpConfigCall().api).toBe("openai-responses"); + expect(httpConfigCall().request).toEqual({ allowPrivateNetwork: true }); + expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations"); + expect(jsonRequestCall().headers?.get("X-Test-OpenAI")).toBe("direct"); + expectNoJsonRequestUrl("https://chatgpt.com/backend-api/codex/responses"); + }); + + it("keeps mixed OpenAI OAuth/API-key image auth on the selected OAuth transport", async () => { + mockCodexImageStream(); + resolveApiKeyForProviderMock.mockResolvedValue({ + apiKey: "codex-key", + source: "profile:openai:chatgpt", + mode: "oauth", + }); + + const provider = buildOpenAIImageGenerationProvider(); + await provider.generateImage({ + provider: "openai", + model: "gpt-image-2", + prompt: "Draw using the selected ChatGPT profile", + cfg: { + auth: { + profiles: { + "openai:chatgpt": { + provider: "openai", + mode: "oauth", + }, + "openai:default": { + provider: "openai", + mode: "api_key", + }, + }, + order: { + openai: ["openai:chatgpt", "openai:default"], + }, + }, + }, + authStore: createMixedOpenAIAuthStore(), + }); + + expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1); + expect(authResolutionCall().provider).toBe("openai"); + expect(httpConfigCall().provider).toBe("openai"); + expect(httpConfigCall().api).toBe("openai-codex-responses"); + expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses"); + }); + it("sends Codex reference images as Responses input images", async () => { mockCodexAuthOnly(); mockCodexImageStream(); diff --git a/extensions/openai/image-generation-provider.ts b/extensions/openai/image-generation-provider.ts index d9f9e0695c5a..dc60a121d494 100644 --- a/extensions/openai/image-generation-provider.ts +++ b/extensions/openai/image-generation-provider.ts @@ -297,46 +297,6 @@ function normalizeProviderId(value: string | undefined): string { return value?.trim().toLowerCase() ?? ""; } -function hasExplicitOpenAIDirectAuthConfig(cfg: OpenClawConfig | undefined): boolean { - const profiles = cfg?.auth?.profiles; - if (!profiles) { - return false; - } - return Object.values(profiles).some( - (profile) => normalizeProviderId(profile.provider) === "openai", - ); -} - -function hasExplicitOpenAIDirectProviderConfig(cfg: OpenClawConfig | undefined): boolean { - if (hasExplicitOpenAIDirectAuthConfig(cfg)) { - return true; - } - const providerConfig = cfg?.models?.providers?.openai; - if (!providerConfig) { - return false; - } - if (providerConfig.apiKey !== undefined) { - return true; - } - const configuredBaseUrl = resolveConfiguredOpenAIBaseUrl(cfg); - if ( - configuredBaseUrl.trim() && - configuredBaseUrl.replace(/\/+$/, "") !== DEFAULT_OPENAI_IMAGE_BASE_URL - ) { - return true; - } - if (providerConfig.api !== undefined) { - return true; - } - if (providerConfig.headers && Object.keys(providerConfig.headers).length > 0) { - return true; - } - if (providerConfig.authHeader === false || providerConfig.request !== undefined) { - return true; - } - return false; -} - function resolveRequestAuthStore(req: { authStore?: AuthProfileStore; agentDir?: string; @@ -353,6 +313,31 @@ function resolveRequestAuthStore(req: { }); } +function hasDirectOpenAIImageApiKeyAuth(params: { + cfg?: OpenClawConfig; + agentDir?: string; +}): boolean { + if (hasExplicitOpenAIImageApiKeyConfig(params.cfg)) { + return true; + } + if (process.env.OPENAI_API_KEY?.trim()) { + return true; + } + const store = params.agentDir + ? ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, + }) + : undefined; + if (!store) { + return false; + } + const profileIds = listProfilesForProvider(store, "openai"); + if (profileIds.length === 0) { + return true; + } + return profileIds.some((profileId) => store.profiles[profileId]?.type === "api_key"); +} + function hasCodexResponseTransportProfileConfigured(req: { authStore?: AuthProfileStore; agentDir?: string; @@ -361,12 +346,123 @@ function hasCodexResponseTransportProfileConfigured(req: { if (!store) { return false; } - return listProfilesForProvider(store, "openai-codex").some( - (profileId) => - store.profiles[profileId]?.type === "oauth" || store.profiles[profileId]?.type === "token", + return ["openai", "openai-codex"].some((provider) => + listProfilesForProvider(store, provider).some( + (profileId) => + store.profiles[profileId]?.type === "oauth" || store.profiles[profileId]?.type === "token", + ), ); } +function resolveOpenAIImageAuthProvider(req: { + cfg?: OpenClawConfig; + authStore?: AuthProfileStore; + agentDir?: string; +}): string { + const providerConfig = req.cfg?.models?.providers?.openai; + if (providerConfig?.apiKey !== undefined || providerConfig?.auth === "api-key") { + return "openai"; + } + const store = resolveRequestAuthStore(req); + if (!store) { + return "openai"; + } + const profiles = Object.values(store.profiles); + const hasCanonicalProfiles = profiles.some( + (profile) => normalizeProviderId(profile.provider) === "openai", + ); + const hasLegacySubscriptionProfile = profiles.some( + (profile) => + normalizeProviderId(profile.provider) === "openai-codex" && + (profile.type === "oauth" || profile.type === "token"), + ); + return !hasCanonicalProfiles && hasLegacySubscriptionProfile ? "openai-codex" : "openai"; +} + +function hasExplicitOpenAIImageApiKeyConfig(cfg: OpenClawConfig | undefined): boolean { + const providerConfig = cfg?.models?.providers?.openai; + return providerConfig?.apiKey !== undefined || providerConfig?.auth === "api-key"; +} + +function hasExplicitDirectOpenAIImageConfig(cfg: OpenClawConfig | undefined): boolean { + const providerConfig = cfg?.models?.providers?.openai; + if (!providerConfig) { + return false; + } + return ( + hasExplicitOpenAIImageApiKeyConfig(cfg) || + providerConfig.headers !== undefined || + providerConfig.authHeader !== undefined || + providerConfig.request !== undefined || + (providerConfig.api !== undefined && providerConfig.api !== "openai-codex-responses") + ); +} + +function resolveConfiguredOpenAIImageHeaders( + cfg: OpenClawConfig | undefined, +): Record | undefined { + const headers = cfg?.models?.providers?.openai?.headers; + if (!headers) { + return undefined; + } + const stringHeaders = Object.fromEntries( + Object.entries(headers).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ), + ); + return Object.keys(stringHeaders).length > 0 ? stringHeaders : undefined; +} + +function forceOpenAIImageApiKeyAuth(cfg: OpenClawConfig | undefined): OpenClawConfig | undefined { + if (!hasExplicitOpenAIImageApiKeyConfig(cfg)) { + return cfg; + } + const providerConfig = cfg?.models?.providers?.openai; + if (!providerConfig) { + return cfg; + } + return { + ...cfg, + models: { + ...cfg?.models, + providers: { + ...cfg?.models?.providers, + openai: { + ...providerConfig, + auth: "api-key", + }, + }, + }, + }; +} + +async function resolveOpenAIImageAuth(req: { + cfg?: OpenClawConfig; + agentDir?: string; + authStore?: AuthProfileStore; +}) { + const provider = resolveOpenAIImageAuthProvider(req); + const primary = await resolveOptionalApiKeyForProvider({ + provider, + cfg: req.cfg, + agentDir: req.agentDir, + store: req.authStore, + }); + if (primary?.apiKey || provider === "openai-codex") { + return primary; + } + return await resolveOptionalApiKeyForProvider({ + provider: "openai-codex", + cfg: req.cfg, + agentDir: req.agentDir, + store: req.authStore, + }); +} + +function isCodexSubscriptionAuthMode(mode: unknown): boolean { + return mode === "oauth" || mode === "token"; +} + type OpenAICodexImageGenerationEvent = { type?: string; item?: { @@ -550,7 +646,6 @@ function createOpenAIImageGenerationProviderBase(params: { }): ImageGenerationProvider { return { id: params.id, - aliases: ["openai-codex"], label: params.label, defaultModel: DEFAULT_OPENAI_IMAGE_MODEL, models: [...OPENAI_IMAGE_MODELS], @@ -607,7 +702,7 @@ function logCodexImageAuthSelected(params: { allowTransparentDefaultReroute: true, }); log.info( - `image auth selected: provider=openai-codex mode=${sanitizeLogValue( + `image auth selected: provider=openai mode=${sanitizeLogValue( params.authMode, )} transport=codex-responses requestedModel=${sanitizeLogValue( model, @@ -621,7 +716,12 @@ async function generateOpenAICodexImage(params: { }): Promise { const { req, apiKey } = params; const inputImages = req.inputImages ?? []; - const codexProviderConfig = req.cfg?.models?.providers?.["openai-codex"]; + const openAIProviderConfig = req.cfg?.models?.providers?.openai; + const legacyCodexProviderConfig = req.cfg?.models?.providers?.["openai-codex"]; + const codexProviderConfig = + openAIProviderConfig?.api === "openai-codex-responses" + ? openAIProviderConfig + : legacyCodexProviderConfig; const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } = resolveProviderHttpRequestConfig({ baseUrl: canonicalizeCodexResponsesBaseUrl(codexProviderConfig?.baseUrl), @@ -631,7 +731,7 @@ async function generateOpenAICodexImage(params: { Accept: "text/event-stream", }, request: sanitizeConfiguredModelProviderRequest(codexProviderConfig?.request), - provider: "openai-codex", + provider: "openai", api: "openai-codex-responses", capability: "image", transport: "http", @@ -736,34 +836,55 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider { agentDir, }) ) { - return true; + return ( + isPublicOpenAIImageBaseUrl(resolveConfiguredOpenAIBaseUrl(cfg)) || + hasDirectOpenAIImageApiKeyAuth({ cfg, agentDir }) + ); + } + if ( + isProviderApiKeyConfigured({ + provider: "openai-codex", + agentDir, + }) + ) { + return isPublicOpenAIImageBaseUrl(resolveConfiguredOpenAIBaseUrl(cfg)); } if (!isPublicOpenAIImageBaseUrl(resolveConfiguredOpenAIBaseUrl(cfg))) { return false; } - return isProviderApiKeyConfigured({ - provider: "openai-codex", - agentDir, - }); + return false; }, async generateImage(req) { const inputImages = req.inputImages ?? []; const isEdit = inputImages.length > 0; const rawBaseUrl = resolveConfiguredOpenAIBaseUrl(req.cfg); const publicOpenAIBaseUrl = isPublicOpenAIImageBaseUrl(rawBaseUrl); + const explicitOpenAIApiKeyConfig = hasExplicitOpenAIImageApiKeyConfig(req.cfg); + const explicitDirectOpenAIConfig = hasExplicitDirectOpenAIImageConfig(req.cfg); const useCodexResponseTransportRoute = publicOpenAIBaseUrl && - !hasExplicitOpenAIDirectProviderConfig(req.cfg) && + !explicitDirectOpenAIConfig && hasCodexResponseTransportProfileConfigured(req); - let preResolvedImageAuth: Awaited> | undefined; - if (useCodexResponseTransportRoute) { - const codexAuth = await resolveApiKeyForProvider({ - provider: "openai-codex", - cfg: req.cfg, + let preResolvedImageAuth: + | NonNullable>> + | null + | undefined; + if (explicitOpenAIApiKeyConfig) { + const directAuth = await resolveOptionalApiKeyForProvider({ + provider: "openai", + cfg: forceOpenAIImageApiKeyAuth(req.cfg), agentDir: req.agentDir, store: req.authStore, + credentialPrecedence: "env-first", }); - if (!codexAuth.apiKey) { + preResolvedImageAuth = + directAuth?.apiKey && (directAuth.mode === undefined || directAuth.mode === "api-key") + ? directAuth + : null; + } + if (useCodexResponseTransportRoute) { + const codexAuth = await resolveOpenAIImageAuth(req); + if (!codexAuth?.apiKey) { throw new Error("OpenAI Codex OAuth missing"); } if (codexAuth.mode === "api-key") { @@ -775,45 +896,61 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider { } } - const auth = - preResolvedImageAuth ?? - (await resolveOptionalApiKeyForProvider({ - provider: "openai", - cfg: req.cfg, - agentDir: req.agentDir, - store: req.authStore, - })); - let imageAuth = auth; - if (!imageAuth?.apiKey) { - if (!publicOpenAIBaseUrl) { - throw new Error("OpenAI API key missing"); + let imageAuth: + | NonNullable>> + | null + | undefined = + preResolvedImageAuth !== undefined + ? preResolvedImageAuth + : await resolveOptionalApiKeyForProvider({ + provider: "openai", + cfg: req.cfg, + agentDir: req.agentDir, + store: req.authStore, + }); + if ( + !explicitDirectOpenAIConfig && + imageAuth?.apiKey && + isCodexSubscriptionAuthMode(imageAuth.mode) + ) { + if (publicOpenAIBaseUrl) { + const timeoutMs = resolveOpenAIImageTimeoutMs(req.timeoutMs); + logCodexImageAuthSelected({ req, authMode: imageAuth.mode, timeoutMs }); + return generateOpenAICodexImage({ req, apiKey: imageAuth.apiKey }); } - const codexAuth = await resolveOptionalApiKeyForProvider({ + imageAuth = undefined; + } + if (!imageAuth?.apiKey && publicOpenAIBaseUrl && !explicitDirectOpenAIConfig) { + const legacyCodexAuth = await resolveOptionalApiKeyForProvider({ provider: "openai-codex", cfg: req.cfg, agentDir: req.agentDir, store: req.authStore, }); - if (codexAuth?.apiKey) { - if (codexAuth.mode === "api-key") { - imageAuth = codexAuth; - } else { - const timeoutMs = resolveOpenAIImageTimeoutMs(req.timeoutMs); - logCodexImageAuthSelected({ req, authMode: codexAuth.mode, timeoutMs }); - return generateOpenAICodexImage({ req, apiKey: codexAuth.apiKey }); - } + if (legacyCodexAuth?.apiKey && isCodexSubscriptionAuthMode(legacyCodexAuth.mode)) { + const timeoutMs = resolveOpenAIImageTimeoutMs(req.timeoutMs); + logCodexImageAuthSelected({ req, authMode: legacyCodexAuth.mode, timeoutMs }); + return generateOpenAICodexImage({ req, apiKey: legacyCodexAuth.apiKey }); } - if (!imageAuth?.apiKey) { - throw new Error("OpenAI API key or Codex OAuth missing"); + imageAuth = legacyCodexAuth; + } + if (!imageAuth?.apiKey) { + if (!publicOpenAIBaseUrl) { + throw new Error("OpenAI API key missing"); } + throw new Error("OpenAI API key or Codex OAuth missing"); } const isAzure = isAzureOpenAIBaseUrl(rawBaseUrl); + const openAIProviderConfig = req.cfg?.models?.providers?.openai; const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } = resolveProviderHttpRequestConfig({ baseUrl: rawBaseUrl, defaultBaseUrl: DEFAULT_OPENAI_IMAGE_BASE_URL, allowPrivateNetwork: shouldAllowPrivateImageEndpoint(req), + headers: resolveConfiguredOpenAIImageHeaders(req.cfg), + request: sanitizeConfiguredModelProviderRequest(openAIProviderConfig?.request), + api: openAIProviderConfig?.api, defaultHeaders: isAzure ? { "api-key": imageAuth.apiKey } : { Authorization: `Bearer ${imageAuth.apiKey}` }, diff --git a/extensions/openai/index.test.ts b/extensions/openai/index.test.ts index e13858d11108..7cdab0be348a 100644 --- a/extensions/openai/index.test.ts +++ b/extensions/openai/index.test.ts @@ -329,10 +329,9 @@ describe("openai plugin", () => { ).toBeLessThan(runtimeMocks.refreshOpenAICodexToken.mock.invocationCallOrder[0]); }); - it("registers provider-owned OpenAI tool compat hooks for openai and codex", async () => { + it("registers provider-owned OpenAI tool compat hooks for API and Codex transports", async () => { const { providers } = await registerOpenAIPluginWithHook(); const openaiProvider = requireRegisteredProvider(providers, "openai"); - const codexProvider = requireRegisteredProvider(providers, "openai-codex"); const noParamsTool = { name: "ping", description: "", @@ -352,12 +351,12 @@ describe("openai plugin", () => { } as never, tools: [noParamsTool], } as never); - const normalizedCodex = codexProvider.normalizeToolSchemas?.({ - provider: "openai-codex", + const normalizedCodex = openaiProvider.normalizeToolSchemas?.({ + provider: "openai", modelId: "gpt-5.4", modelApi: "openai-codex-responses", model: { - provider: "openai-codex", + provider: "openai", api: "openai-codex-responses", baseUrl: "https://chatgpt.com/backend-api", id: "gpt-5.4", @@ -392,12 +391,12 @@ describe("openai plugin", () => { } as never), ).toStrictEqual([]); expect( - codexProvider.inspectToolSchemas?.({ - provider: "openai-codex", + openaiProvider.inspectToolSchemas?.({ + provider: "openai", modelId: "gpt-5.4", modelApi: "openai-codex-responses", model: { - provider: "openai-codex", + provider: "openai", api: "openai-codex-responses", baseUrl: "https://chatgpt.com/backend-api", id: "gpt-5.4", @@ -415,7 +414,6 @@ describe("openai plugin", () => { expectNoBeforePromptBuildHook(on); const openaiProvider = requireRegisteredProvider(providers, "openai"); - const codexProvider = requireRegisteredProvider(providers, "openai-codex"); const contributionContext: Parameters< NonNullable >[0] = { @@ -444,12 +442,6 @@ describe("openai plugin", () => { expect(OPENAI_FRIENDLY_PROMPT_OVERLAY).toContain( "Occasional emoji are fine when they fit naturally, especially for warmth or brief celebration; keep them sparse.", ); - expect(codexProvider.resolveSystemPromptContribution?.(contributionContext)).toEqual({ - stablePrefix: OPENAI_GPT5_BEHAVIOR_CONTRACT, - sectionOverrides: { - interaction_style: OPENAI_FRIENDLY_PROMPT_OVERLAY, - }, - }); expect( openaiProvider.resolveSystemPromptContribution?.({ ...contributionContext, diff --git a/extensions/openai/index.ts b/extensions/openai/index.ts index e3e8d8ab2435..6f10c81b3efd 100644 --- a/extensions/openai/index.ts +++ b/extensions/openai/index.ts @@ -2,12 +2,8 @@ import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-run import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js"; -import { - openaiCodexMediaUnderstandingProvider, - openaiMediaUnderstandingProvider, -} from "./media-understanding-provider.js"; +import { openaiMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { openAiMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; -import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js"; import { buildOpenAIProvider } from "./openai-provider.js"; import { resolveOpenAIPromptOverlayMode, @@ -45,14 +41,12 @@ export default definePluginEntry({ }, }); api.registerProvider(buildProviderWithPromptContribution(buildOpenAIProvider())); - api.registerProvider(buildProviderWithPromptContribution(buildOpenAICodexProviderPlugin())); api.registerMemoryEmbeddingProvider(openAiMemoryEmbeddingProviderAdapter); api.registerImageGenerationProvider(buildOpenAIImageGenerationProvider()); api.registerRealtimeTranscriptionProvider(buildOpenAIRealtimeTranscriptionProvider()); api.registerRealtimeVoiceProvider(buildOpenAIRealtimeVoiceProvider()); api.registerSpeechProvider(buildOpenAISpeechProvider()); api.registerMediaUnderstandingProvider(openaiMediaUnderstandingProvider); - api.registerMediaUnderstandingProvider(openaiCodexMediaUnderstandingProvider); api.registerVideoGenerationProvider(buildOpenAIVideoGenerationProvider()); }, }); diff --git a/extensions/openai/media-understanding-provider.test.ts b/extensions/openai/media-understanding-provider.test.ts index 6b81ca8f0b15..e6c156b5812d 100644 --- a/extensions/openai/media-understanding-provider.test.ts +++ b/extensions/openai/media-understanding-provider.test.ts @@ -5,22 +5,21 @@ import { } from "openclaw/plugin-sdk/test-env"; import { describe, expect, it } from "vitest"; import { - openaiCodexMediaUnderstandingProvider, + openaiMediaUnderstandingProvider, transcribeOpenAiAudio, - transcribeOpenAiCodexAudio, } from "./media-understanding-provider.js"; installPinnedHostnameTestHooks(); -describe("openaiCodexMediaUnderstandingProvider", () => { +describe("openaiMediaUnderstandingProvider", () => { it("declares audio support with the transcription default", () => { - expect(openaiCodexMediaUnderstandingProvider.capabilities).toEqual(["image", "audio"]); - expect(openaiCodexMediaUnderstandingProvider.defaultModels).toEqual({ + expect(openaiMediaUnderstandingProvider.capabilities).toEqual(["image", "audio"]); + expect(openaiMediaUnderstandingProvider.defaultModels).toEqual({ image: "gpt-5.5", audio: "gpt-4o-transcribe", }); - expect(openaiCodexMediaUnderstandingProvider.autoPriority).toEqual({ image: 20, audio: 20 }); - expect(openaiCodexMediaUnderstandingProvider.transcribeAudio).toBe(transcribeOpenAiCodexAudio); + expect(openaiMediaUnderstandingProvider.autoPriority).toEqual({ image: 20, audio: 20 }); + expect(openaiMediaUnderstandingProvider.transcribeAudio).toBe(transcribeOpenAiAudio); }); }); @@ -98,22 +97,3 @@ describe("transcribeOpenAiAudio", () => { ).rejects.toThrow("Audio transcription response missing text"); }); }); - -describe("transcribeOpenAiCodexAudio", () => { - it("uses the OpenAI transcription default through the Codex provider id", async () => { - const { fetchFn, getRequest } = createRequestCaptureJsonFetch({ text: "hello" }); - - const result = await transcribeOpenAiCodexAudio({ - buffer: Buffer.from("audio-bytes"), - fileName: "voice.wav", - apiKey: "test-key", - timeoutMs: 1234, - model: " ", - fetchFn, - }); - - const form = getRequest().init?.body as FormData; - expect(result.model).toBe("gpt-4o-transcribe"); - expect(form.get("model")).toBe("gpt-4o-transcribe"); - }); -}); diff --git a/extensions/openai/media-understanding-provider.ts b/extensions/openai/media-understanding-provider.ts index e09cc5d0e14d..86e705377598 100644 --- a/extensions/openai/media-understanding-provider.ts +++ b/extensions/openai/media-understanding-provider.ts @@ -18,34 +18,12 @@ export async function transcribeOpenAiAudio(params: AudioTranscriptionRequest) { }); } -export async function transcribeOpenAiCodexAudio(params: AudioTranscriptionRequest) { - return await transcribeOpenAiCompatibleAudio({ - ...params, - provider: "openai-codex", - defaultBaseUrl: DEFAULT_OPENAI_AUDIO_BASE_URL, - defaultModel: OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL, - }); -} - export const openaiMediaUnderstandingProvider: MediaUnderstandingProvider = { id: "openai", capabilities: ["image", "audio"], - defaultModels: { - image: "gpt-5.4-mini", - audio: OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL, - }, - autoPriority: { image: 10, audio: 10 }, - describeImage: describeImageWithModel, - describeImages: describeImagesWithModel, - transcribeAudio: transcribeOpenAiAudio, -}; - -export const openaiCodexMediaUnderstandingProvider: MediaUnderstandingProvider = { - id: "openai-codex", - capabilities: ["image", "audio"], defaultModels: { image: "gpt-5.5", audio: OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL }, autoPriority: { image: 20, audio: 20 }, describeImage: describeImageWithModel, describeImages: describeImagesWithModel, - transcribeAudio: transcribeOpenAiCodexAudio, + transcribeAudio: transcribeOpenAiAudio, }; diff --git a/extensions/openai/openai-codex-oauth.runtime.ts b/extensions/openai/openai-codex-oauth.runtime.ts index 9bf420ea1e8b..12036c8de36e 100644 --- a/extensions/openai/openai-codex-oauth.runtime.ts +++ b/extensions/openai/openai-codex-oauth.runtime.ts @@ -183,7 +183,7 @@ function rewriteOpenAICodexOAuthError(error: unknown): Error { "unsupported_region", [ "OpenAI rejected the token exchange for this country, region, or network route.", - "If you normally use a proxy, verify HTTPS_PROXY, HTTP_PROXY, or ALL_PROXY is set for the OpenClaw process and then retry `openclaw models auth login --provider openai-codex`.", + "If you normally use a proxy, verify HTTPS_PROXY, HTTP_PROXY, or ALL_PROXY is set for the OpenClaw process and then retry `openclaw models auth login --provider openai`.", ].join(" "), error, ); diff --git a/extensions/openai/openai-codex-provider.runtime.ts b/extensions/openai/openai-codex-provider.runtime.ts index 66e5c384d7a0..6e24ab134e65 100644 --- a/extensions/openai/openai-codex-provider.runtime.ts +++ b/extensions/openai/openai-codex-provider.runtime.ts @@ -46,10 +46,10 @@ async function getOpenAICodexOAuthApiKey( providerId: string, credentials: Record, ): Promise<{ newCredentials: OAuthCredentials; apiKey: string } | null> { - if (providerId !== "openai-codex") { + if (providerId !== "openai" && providerId !== "openai-codex") { throw new Error(`Unknown OAuth provider: ${providerId}`); } - let creds = credentials[providerId]; + let creds = credentials[providerId] ?? credentials["openai-codex"]; if (!creds) { return null; } diff --git a/extensions/openai/openai-codex-provider.test.ts b/extensions/openai/openai-codex-provider.test.ts index a411b211eacd..ec6a18671e29 100644 --- a/extensions/openai/openai-codex-provider.test.ts +++ b/extensions/openai/openai-codex-provider.test.ts @@ -11,93 +11,11 @@ vi.mock("./openai-codex-device-code.js", () => ({ loginOpenAICodexDeviceCode: loginOpenAICodexDeviceCodeMock, })); -let buildOpenAICodexProviderPlugin: typeof import("./openai-codex-provider.js").buildOpenAICodexProviderPlugin; +let buildOpenAIProvider: typeof import("./openai-provider.js").buildOpenAIProvider; -function createCodexTemplate(overrides: { - id?: string; - name?: string; - cost?: { input: number; output: number; cacheRead: number; cacheWrite: number }; - contextWindow?: number; - contextTokens?: number; -}) { - return { - id: overrides.id ?? "gpt-5.3-codex", - name: overrides.name ?? overrides.id ?? "gpt-5.3-codex", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"] as const, - cost: overrides.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: overrides.contextWindow ?? 272_000, - ...(overrides.contextTokens === undefined ? {} : { contextTokens: overrides.contextTokens }), - maxTokens: 128_000, - }; -} - -function createSingleModelRegistry( - template: ReturnType, - missValue?: null, -) { - return { - find: (providerId: string, modelId: string) => - providerId === "openai-codex" && modelId === template.id ? template : missValue, - }; -} - -type CodexProvider = ReturnType; -type CodexAuthMethod = NonNullable[number]; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function requireRecord(value: unknown, label: string): Record { - if (!isRecord(value)) { - throw new Error(`expected ${label} to be an object`); - } - return value; -} - -function requireArray(value: unknown, label: string): unknown[] { - if (!Array.isArray(value)) { - throw new Error(`expected ${label} to be an array`); - } - return value; -} - -function requireAuthMethod(provider: CodexProvider, id: string): CodexAuthMethod { - const method = provider.auth?.find((candidate) => candidate.id === id); - if (!method) { - throw new Error(`expected OpenAI Codex auth method ${id}`); - } - return method; -} - -function expectRecordFields(value: unknown, label: string, expected: Record) { - const record = requireRecord(value, label); - for (const [key, expectedValue] of Object.entries(expected)) { - expect(record[key]).toEqual(expectedValue); - } -} - -function expectModelFields(model: unknown, expected: Record) { - expectRecordFields(model, "model", expected); -} - -function requireEntryById(entries: unknown, id: string): Record { - const entry = requireArray(entries, "model catalog entries") - .filter(isRecord) - .find((candidate) => candidate.id === id); - if (!entry) { - throw new Error(`expected model catalog entry ${id}`); - } - return entry; -} - -describe("openai codex provider", () => { +describe("OpenAI provider Codex transport hooks", () => { beforeAll(async () => { - ({ buildOpenAICodexProviderPlugin } = await import("./openai-codex-provider.js")); + ({ buildOpenAIProvider } = await import("./openai-provider.js")); }); beforeEach(() => { @@ -105,866 +23,163 @@ describe("openai codex provider", () => { loginOpenAICodexDeviceCodeMock.mockReset(); }); - it("falls back to the cached credential when accountId extraction fails", async () => { - const provider = buildOpenAICodexProviderPlugin(); - const credential = { - type: "oauth" as const, - provider: "openai-codex", - access: "cached-access-token", - refresh: "refresh-token", - expires: Date.now() - 60_000, - }; - refreshOpenAICodexTokenMock.mockRejectedValueOnce( - new Error("Failed to extract accountId from token"), - ); - - await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(credential); - }); - - it("rethrows unrelated refresh failures", async () => { - const provider = buildOpenAICodexProviderPlugin(); - const credential = { - type: "oauth" as const, - provider: "openai-codex", - access: "cached-access-token", - refresh: "refresh-token", - expires: Date.now() - 60_000, - }; - refreshOpenAICodexTokenMock.mockRejectedValueOnce(new Error("invalid_grant")); - - await expect(provider.refreshOAuth?.(credential)).rejects.toThrow("invalid_grant"); - }); - - it("merges refreshed oauth credentials", async () => { - const provider = buildOpenAICodexProviderPlugin(); - const credential = { - type: "oauth" as const, - provider: "openai-codex", - access: "cached-access-token", - refresh: "refresh-token", - expires: Date.now() - 60_000, - email: "user@example.com", - displayName: "User", - }; - refreshOpenAICodexTokenMock.mockResolvedValueOnce({ - access: "next-access", - refresh: "next-refresh", - expires: Date.now() + 60_000, - }); - - const refreshed = await provider.refreshOAuth?.(credential); - - expect(refreshed?.expires).toBeTypeOf("number"); - expect(refreshed).toEqual({ - ...credential, - access: "next-access", - refresh: "next-refresh", - expires: refreshed?.expires, - }); - }); - - it("exposes grouped model/auth picker labels for Codex auth methods", () => { - const provider = buildOpenAICodexProviderPlugin(); - const oauth = requireAuthMethod(provider, "oauth"); - const deviceCode = requireAuthMethod(provider, "device-code"); - const apiKey = requireAuthMethod(provider, "api-key"); - - expectRecordFields(oauth.wizard, "oauth wizard", { - choiceLabel: "ChatGPT/Codex Browser Login", - groupId: "openai", - groupLabel: "OpenAI", - groupHint: "ChatGPT/Codex sign-in or API key", - }); - expectRecordFields(deviceCode.wizard, "device-code wizard", { - choiceLabel: "ChatGPT/Codex Device Pairing", - groupId: "openai", - groupLabel: "OpenAI", - groupHint: "ChatGPT/Codex sign-in or API key", - }); - expectRecordFields(apiKey.wizard, "api-key wizard", { - choiceLabel: "OpenAI API Key Backup", - choiceHint: "Use an OpenAI API key when your Codex subscription is unavailable", - groupId: "openai", - groupLabel: "OpenAI", - groupHint: "ChatGPT/Codex sign-in or API key", - }); - }); - - it("returns deprecated-profile doctor guidance for legacy Codex CLI ids", () => { - const provider = buildOpenAICodexProviderPlugin(); - - expect( - provider.buildAuthDoctorHint?.({ - provider: "openai-codex", - profileId: "openai-codex:codex-cli", - config: undefined, - store: { version: 1, profiles: {} }, - }), - ).toBe( - "Deprecated profile. Run `openclaw models auth login --provider openai-codex` or `openclaw configure`.", - ); - }); - - it("declares the legacy default OAuth profile repair", () => { - const provider = buildOpenAICodexProviderPlugin(); + it("exposes ChatGPT OAuth on the canonical OpenAI provider", () => { + const provider = buildOpenAIProvider(); + expect(provider.id).toBe("openai"); + expect(provider.aliases).toBeUndefined(); + expect(provider.hookAliases).toEqual([ + "openai-codex", + "azure-openai", + "azure-openai-responses", + ]); + expect(provider.auth?.map((method) => method.id)).toEqual(["oauth", "device-code", "api-key"]); + expect(provider.auth?.map((method) => method.wizard?.choiceId)).toEqual([ + "openai", + "openai-device-code", + "openai-api-key", + ]); expect(provider.oauthProfileIdRepairs).toEqual([ { legacyProfileId: "openai-codex:default", - promptLabel: "OpenAI Codex", + promptLabel: "OpenAI", }, ]); }); - it("offers OpenAI menu auth methods for browser login and device pairing", () => { - const provider = buildOpenAICodexProviderPlugin(); - const oauth = requireAuthMethod(provider, "oauth"); - const deviceCode = requireAuthMethod(provider, "device-code"); - - expect(provider.auth?.map((method) => method.id)).toEqual(["oauth", "device-code", "api-key"]); - expect(oauth.label).toBe("ChatGPT/Codex Browser Login"); - expect(oauth.hint).toBe("Sign in with OpenAI in your browser"); - expectRecordFields(oauth.wizard, "oauth wizard", { - choiceId: "openai-codex", - choiceLabel: "ChatGPT/Codex Browser Login", - assistantPriority: -30, - onboardingFeatured: true, - }); - expect(deviceCode.label).toBe("ChatGPT/Codex Device Pairing"); - expect(deviceCode.hint).toBe("Pair in browser with a device code"); - expect(deviceCode.kind).toBe("device_code"); - expectRecordFields(deviceCode.wizard, "device-code wizard", { - choiceId: "openai-codex-device-code", - choiceLabel: "ChatGPT/Codex Device Pairing", - assistantPriority: -10, - }); - }); - - it("stores device-code logins as OpenAI Codex oauth profiles", async () => { - const provider = buildOpenAICodexProviderPlugin(); - const deviceCodeMethod = requireAuthMethod(provider, "device-code"); - const note = vi.fn(async () => {}); - const progress = { update: vi.fn(), stop: vi.fn() }; - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; + it("stores device-code logins as OpenAI OAuth profiles", async () => { + const provider = buildOpenAIProvider(); + const deviceCodeMethod = provider.auth?.find((method) => method.id === "device-code"); loginOpenAICodexDeviceCodeMock.mockResolvedValueOnce({ - access: - "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdC1kZXZpY2UtMTIzIn19.signature", - refresh: "device-refresh-token", - expires: Date.now() + 60_000, + access: "access-token", + refresh: "refresh-token", + expires: 1_700_000_000_000, }); - const result = await deviceCodeMethod.run({ - config: {}, - env: process.env, - prompter: { - note, - progress: vi.fn(() => progress), - } as never, - runtime: runtime as never, + const result = await deviceCodeMethod?.run({ isRemote: false, - openUrl: async () => {}, - oauth: { createVpsAwareHandlers: (() => ({})) as never }, - }); + openUrl: vi.fn(async () => {}), + prompter: { + note: vi.fn(async () => {}), + progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })), + }, + runtime: { log: vi.fn(), error: vi.fn() }, + config: {}, + oauth: {}, + } as never); - expect(loginOpenAICodexDeviceCodeMock).toHaveBeenCalledOnce(); - expect(runtime.error).not.toHaveBeenCalled(); - expect(note).not.toHaveBeenCalledWith( - "Trouble with device code login? See https://docs.openclaw.ai/start/faq", - "OAuth help", - ); - const profiles = requireArray(result?.profiles, "device-code profiles"); - expect(profiles).toHaveLength(1); - const profile = requireRecord(profiles[0], "device-code profile"); - const credential = requireRecord(profile.credential, "device-code credential"); - expect(profile.profileId).toBe("openai-codex:default"); - expect(credential).toEqual({ - type: "oauth", - provider: "openai-codex", - access: - "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdC1kZXZpY2UtMTIzIn19.signature", - refresh: "device-refresh-token", - expires: credential.expires, - accountId: "acct-device-123", - }); - expect(credential.expires).toBeTypeOf("number"); - expect(result?.defaultModel).toBe("openai/gpt-5.5"); - expect(result?.configPatch).toEqual({ - agents: { - defaults: { - models: { - "openai/gpt-5.5": {}, - }, - }, + expect(result?.profiles?.[0]).toMatchObject({ + profileId: "openai:default", + credential: { + type: "oauth", + provider: "openai", + access: "access-token", + refresh: "refresh-token", }, }); - expect(result?.profiles[0]?.credential).not.toHaveProperty("idToken"); }); - async function runRemoteDeviceCodeAuthFlow() { - const provider = buildOpenAICodexProviderPlugin(); - const deviceCodeMethod = requireAuthMethod(provider, "device-code"); - const note = vi.fn(async () => {}); - const progress = { update: vi.fn(), stop: vi.fn() }; - const runtime = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - loginOpenAICodexDeviceCodeMock.mockImplementationOnce(async ({ onVerification }) => { - await onVerification({ - verificationUrl: "https://auth.openai.com/codex/device", - userCode: "CODE-12345", - expiresInMs: 900_000, - }); - return { - access: - "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdC1kZXZpY2UtMTIzIn19.signature", - refresh: "device-refresh-token", - expires: Date.now() + 60_000, - }; - }); - - const result = await deviceCodeMethod.run({ - config: {}, - env: process.env, - prompter: { - note, - progress: vi.fn(() => progress), - } as never, - runtime: runtime as never, - isRemote: true, - openUrl: async () => {}, - oauth: { createVpsAwareHandlers: (() => ({})) as never }, - }); - - expect( - requireArray(result.profiles, "remote device-code profiles").map( - (profile) => requireRecord(profile, "remote device-code profile").profileId, - ), - ).toContain("openai-codex:default"); - - return { note, runtime }; - } - - it("surfaces the device pairing code via the prompter note in remote (SSH) mode (#74212)", async () => { - const { note } = await runRemoteDeviceCodeAuthFlow(); - - expect(note).toHaveBeenCalledWith( - [ - "Open this URL in your LOCAL browser and enter the code below.", - "URL: https://auth.openai.com/codex/device", - "Code: CODE-12345", - "Code expires in 15 minutes. Never share it.", - ].join("\n"), - "OpenAI Codex device code", - ); - expect(note).toHaveBeenCalledTimes(1); - }); - - it("does not write the device pairing code to the runtime log in remote mode", async () => { - const { runtime } = await runRemoteDeviceCodeAuthFlow(); - - const logOutput = runtime.log.mock.calls.flat().join("\n"); - expect(logOutput).toContain("https://auth.openai.com/codex/device"); - expect(logOutput).not.toContain("CODE-12345"); - }); - - it("owns native reasoning output mode for Codex responses", () => { - const provider = buildOpenAICodexProviderPlugin(); - - expect( - provider.resolveReasoningOutputMode?.({ - provider: "openai-codex", - modelApi: "openai-codex-responses", - modelId: "gpt-5.4", - } as never), - ).toBe("native"); - }); - - it("resolves gpt-5.4 with native contextWindow plus default contextTokens cap", () => { - const provider = buildOpenAICodexProviderPlugin(); + it("routes Codex-backed OpenAI models through the Codex Responses transport", () => { + const provider = buildOpenAIProvider(); const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.4", - modelRegistry: createSingleModelRegistry(createCodexTemplate({})) as never, - }); - - expectModelFields(model, { - id: "gpt-5.4", - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }); - }); - - it("resolves gpt-5.4-pro with pro pricing and codex-sized limits", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.4-pro", - modelRegistry: createSingleModelRegistry(createCodexTemplate({})) as never, - }); - - expectModelFields(model, { - id: "gpt-5.4-pro", - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - cost: { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 }, - }); - }); - - it("keeps OpenClaw cost metadata but applies Codex context metadata for gpt-5.5", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.5", - modelRegistry: createSingleModelRegistry( - createCodexTemplate({ - id: "gpt-5.5", - cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, - contextWindow: 272_000, - }), - ) as never, - }); - const pro = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.5-pro", - modelRegistry: createSingleModelRegistry(createCodexTemplate({ id: "gpt-5.4-pro" })) as never, - }); - - expectModelFields(model, { - id: "gpt-5.5", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - input: ["text", "image"], - contextWindow: 400_000, - contextTokens: 272_000, - maxTokens: 128_000, - cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, - }); - expectModelFields(pro, { - id: "gpt-5.5-pro", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - contextWindow: 1_000_000, - contextTokens: 272_000, - maxTokens: 128_000, - cost: { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 }, - }); - }); - - it("repairs sparse configured gpt-5.5 catalog rows to remain image-capable", () => { - const provider = buildOpenAICodexProviderPlugin(); - const sparseConfiguredModel = { - id: "gpt-5.5", - name: "gpt-5.5", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - reasoning: false, - input: ["text"] as const, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200_000, - maxTokens: 8_192, - }; - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.5", - modelRegistry: { - find: (providerId: string, modelId: string) => - providerId === "openai-codex" && modelId === "gpt-5.5" ? sparseConfiguredModel : null, - } as never, - }); - - expectModelFields(model, { - id: "gpt-5.5", - api: "openai-codex-responses", - input: ["text", "image"], - contextWindow: 400_000, - contextTokens: 200_000, - maxTokens: 8_192, - }); - }); - - it("synthesizes gpt-5.5 when the Codex catalog omits the OAuth row", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.5", - modelRegistry: createSingleModelRegistry(createCodexTemplate({}), null) as never, - }); - - expectModelFields(model, { - id: "gpt-5.5", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - reasoning: true, - input: ["text", "image"], - contextWindow: 400_000, - contextTokens: 272_000, - maxTokens: 128_000, - }); - }); - - it("honors providerConfig.baseUrl in the gpt-5.5 synthesis fallback", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.5", - modelRegistry: createSingleModelRegistry(createCodexTemplate({}), null) as never, - providerConfig: { baseUrl: "http://proxy.local:30400" }, - }); - - expectModelFields(model, { - id: "gpt-5.5", - api: "openai-codex-responses", - baseUrl: "http://proxy.local:30400", - }); - }); - - it("honors providerConfig.baseUrl in the gpt-5.4 synthesis fallback", () => { - const provider = buildOpenAICodexProviderPlugin(); - const emptyRegistry = { find: () => null }; - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.4", - modelRegistry: emptyRegistry as never, - providerConfig: { baseUrl: "http://proxy.local:30400" }, - }); - - expectModelFields(model, { - id: "gpt-5.4", - api: "openai-codex-responses", - baseUrl: "http://proxy.local:30400", - contextWindow: 1_050_000, - maxTokens: 128_000, - }); - }); - - it("resolves gpt-5.4-pro from a gpt-5.4 runtime template when legacy codex rows are absent", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.4-pro", - modelRegistry: createSingleModelRegistry( - createCodexTemplate({ - id: "gpt-5.4", - cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - }), - ) as never, - }); - - expectModelFields(model, { - id: "gpt-5.4-pro", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - cost: { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 }, - }); - }); - - it("resolves the legacy gpt-5.4-codex alias to canonical gpt-5.4", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.4-codex", - modelRegistry: createSingleModelRegistry(createCodexTemplate({})) as never, - }); - - expectModelFields(model, { - id: "gpt-5.4", - name: "gpt-5.4", - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }); - }); - - it("resolves gpt-5.4-mini through the Codex OAuth route", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.resolveDynamicModel?.({ - provider: "openai-codex", - modelId: "gpt-5.4-mini", - modelRegistry: createSingleModelRegistry( - createCodexTemplate({ - id: "gpt-5.4", - cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - }), - null, - ) as never, + providerConfig: { api: "openai-codex-responses" }, + modelRegistry: { find: () => null }, } as never); - expectModelFields(model, { - id: "gpt-5.4-mini", - name: "gpt-5.4-mini", + expect(model).toMatchObject({ + provider: "openai", + id: "gpt-5.4", api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - contextWindow: 400_000, - contextTokens: 272_000, - maxTokens: 128_000, - cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, + baseUrl: "https://chatgpt.com/backend-api/codex", }); }); - it("augments catalog with gpt-5.5-pro and gpt-5.4 native metadata", () => { - const provider = buildOpenAICodexProviderPlugin(); + it("keeps default Codex-backed OpenAI catalog models on the Codex Responses transport", () => { + const provider = buildOpenAIProvider(); - const entries = provider.augmentModelCatalog?.({ - env: process.env, - entries: [ - { - id: "gpt-5.3-codex", - name: "gpt-5.3-codex", - provider: "openai-codex", + const model = provider.resolveDynamicModel?.({ + provider: "openai", + modelId: "gpt-5.5", + providerConfig: { api: "openai-codex-responses" }, + modelRegistry: { + find: () => ({ + provider: "openai", + id: "gpt-5.5", + name: "gpt-5.5", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text", "image"], - contextWindow: 272_000, - }, - ], + cost: { input: 1, output: 1, cacheRead: 1, cacheWrite: 1 }, + contextWindow: 400_000, + maxTokens: 128_000, + }), + }, } as never); - expect( - requireArray(entries, "model catalog entries").some( - (entry) => isRecord(entry) && entry.id === "gpt-5.5", - ), - ).toBe(false); - expectRecordFields(requireEntryById(entries, "gpt-5.5-pro"), "gpt-5.5-pro entry", { - contextWindow: 1_000_000, - contextTokens: 272_000, - cost: { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 }, - }); - expectRecordFields(requireEntryById(entries, "gpt-5.4"), "gpt-5.4 entry", { - contextWindow: 1_050_000, - contextTokens: 272_000, - cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, - }); - expectRecordFields(requireEntryById(entries, "gpt-5.4-pro"), "gpt-5.4-pro entry", { - contextWindow: 1_050_000, - contextTokens: 272_000, - cost: { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 }, - }); - expectRecordFields(requireEntryById(entries, "gpt-5.4-mini"), "gpt-5.4-mini entry", { - contextWindow: 400_000, - contextTokens: 272_000, - cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, + expect(model).toMatchObject({ + provider: "openai", + id: "gpt-5.5", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", }); }); - it("augments gpt-5.4-pro from catalog gpt-5.4 when legacy codex rows are absent", () => { - const provider = buildOpenAICodexProviderPlugin(); + it("keeps cloned Codex-backed OpenAI models on the Codex Responses transport", () => { + const provider = buildOpenAIProvider(); - const entries = provider.augmentModelCatalog?.({ - env: process.env, - entries: [ - { + const model = provider.resolveDynamicModel?.({ + provider: "openai", + modelId: "gpt-5.4", + providerConfig: { api: "openai-codex-responses" }, + modelRegistry: { + find: () => ({ + provider: "openai", id: "gpt-5.4", name: "gpt-5.4", - provider: "openai-codex", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text", "image"], - contextWindow: 272_000, - }, - ], - } as never); - - expectRecordFields(requireEntryById(entries, "gpt-5.4-pro"), "gpt-5.4-pro entry", { - contextWindow: 1_050_000, - contextTokens: 272_000, - cost: { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 }, - }); - }); - - it("canonicalizes legacy gpt-5.4-codex models during resolved-model normalization", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - model: { - id: "gpt-5.4-codex", - name: "gpt-5.4-codex", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, + cost: { input: 1, output: 1, cacheRead: 1, cacheWrite: 1 }, + contextWindow: 128_000, + maxTokens: 16_384, + }), }, } as never); - expectModelFields(model, { + expect(model).toMatchObject({ + provider: "openai", id: "gpt-5.4", - name: "gpt-5.4", - }); - }); - - it("defaults missing codex api metadata to openai-codex-responses", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - model: { - id: "gpt-5.4", - name: "gpt-5.4", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - } as never); - - expectModelFields(model, { api: "openai-codex-responses", baseUrl: "https://chatgpt.com/backend-api/codex", }); }); - it("normalizes stale /backend-api/v1 codex metadata to the canonical base url", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - model: { - id: "gpt-5.4", - name: "gpt-5.4", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/v1", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - } as never); - - expectModelFields(model, { - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", + it("refreshes ChatGPT OAuth credentials under the OpenAI provider", async () => { + const provider = buildOpenAIProvider(); + refreshOpenAICodexTokenMock.mockResolvedValueOnce({ + access: "new-access", + refresh: "new-refresh", + expires: 1_700_000_000_000, }); - }); - it("normalizes legacy completions metadata to the codex transport", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - model: { - id: "gpt-5.4", - name: "gpt-5.4", - provider: "openai-codex", - api: "openai-completions", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - } as never); - - expectModelFields(model, { - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", + await expect( + provider.refreshOAuth?.({ + type: "oauth", + provider: "openai", + access: "old-access", + refresh: "old-refresh", + expires: Date.now() - 60_000, + }), + ).resolves.toMatchObject({ + type: "oauth", + provider: "openai", + access: "new-access", + refresh: "new-refresh", }); }); - - it("normalizes legacy GitHub Copilot Codex metadata to the codex transport", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - model: { - id: "gpt-5.4", - name: "gpt-5.4", - provider: "openai-codex", - api: "openai-completions", - baseUrl: "https://api.githubcopilot.com", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - } as never); - - expectModelFields(model, { - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - }); - }); - - it("restores [text,image] input on stale gpt-5.5 rows that omit the input field", () => { - // Regression: persisted/configured catalog rows can omit the `input` field - // for openai-codex models. When that row wins the catalog merge, - // `modelSupportsInput(entry, "image")` returns false and the gateway routes - // inbound images down the claim-check offload path (`media://inbound/`) - // instead of inlining them. Mirror of the Anthropic precedent set by - // upstream #83756. - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - modelId: "gpt-5.5", - model: { - id: "gpt-5.5", - name: "gpt-5.5", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - reasoning: true, - // No `input` field at all — the stale persisted/configured row shape. - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - } as never); - - expect(model?.input).toEqual(["text", "image"]); - }); - - it("preserves [text,image] input on rows that already declare image capability", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - modelId: "gpt-5.5", - model: { - id: "gpt-5.5", - name: "gpt-5.5", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - reasoning: true, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_050_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - } as never); - - // Existing image capability untouched; transport is already canonical so - // normalizeResolvedModel may return undefined (no-op). - if (model) { - expect(model.input).toEqual(["text", "image"]); - } - }); - - it("leaves codex-suffix and legacy model rows text-only (not vision-capable)", () => { - const provider = buildOpenAICodexProviderPlugin(); - - const model = provider.normalizeResolvedModel?.({ - provider: "openai-codex", - modelId: "gpt-5.3-codex", - model: { - id: "gpt-5.3-codex", - name: "gpt-5.3-codex", - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - reasoning: true, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 400_000, - contextTokens: 272_000, - maxTokens: 128_000, - }, - } as never); - - // No image capability should be added — gpt-5.X-codex models are - // Codex CLI / agent-mode only and not in the documented vision list. - // normalizeResolvedModel may return undefined (no transport change) or a - // model with the existing input array; either way `image` must not appear. - if (model?.input) { - expect(model.input).not.toContain("image"); - } - }); - - it("normalizes transport metadata for stale /backend-api/v1 codex routes", () => { - const provider = buildOpenAICodexProviderPlugin(); - - expect( - provider.normalizeTransport?.({ - provider: "openai-codex", - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/v1", - } as never), - ).toEqual({ - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - }); - }); - - it("normalizes transport metadata for legacy completions codex routes", () => { - const provider = buildOpenAICodexProviderPlugin(); - - expect( - provider.normalizeTransport?.({ - provider: "openai-codex", - api: "openai-completions", - baseUrl: "https://api.openai.com/v1", - } as never), - ).toEqual({ - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - }); - }); - - it("normalizes transport metadata for legacy GitHub Copilot Codex routes", () => { - const provider = buildOpenAICodexProviderPlugin(); - - expect( - provider.normalizeTransport?.({ - provider: "openai-codex", - api: "openai-completions", - baseUrl: "https://api.githubcopilot.com/v1", - } as never), - ).toEqual({ - api: "openai-codex-responses", - baseUrl: "https://chatgpt.com/backend-api/codex", - }); - }); - - it("leaves custom proxy completions transport metadata unchanged", () => { - const provider = buildOpenAICodexProviderPlugin(); - - expect( - provider.normalizeTransport?.({ - provider: "openai-codex", - api: "openai-completions", - baseUrl: "https://proxy.example.com/v1", - } as never), - ).toBeUndefined(); - }); }); diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index 87e26a3a0274..fe69952a6a8a 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -1,17 +1,12 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { ProviderAuthContext, + ProviderAuthMethod, ProviderAuthResult, ProviderResolveDynamicModelContext, ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; -import { - CODEX_CLI_PROFILE_ID, - createProviderApiKeyAuthMethod, - ensureAuthProfileStoreForLocalUpdate, - listProfilesForProvider, - type OAuthCredential, -} from "openclaw/plugin-sdk/provider-auth"; +import { CODEX_CLI_PROFILE_ID, type OAuthCredential } from "openclaw/plugin-sdk/provider-auth"; import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; import { DEFAULT_CONTEXT_TOKENS, @@ -26,12 +21,10 @@ import { uniqueValues, } from "openclaw/plugin-sdk/string-coerce-runtime"; import { - OPENAI_CODEX_DEVICE_PAIRING_HINT, - OPENAI_CODEX_DEVICE_PAIRING_LABEL, - OPENAI_CODEX_API_KEY_BACKUP_HINT, - OPENAI_CODEX_API_KEY_BACKUP_LABEL, - OPENAI_CODEX_LOGIN_HINT, - OPENAI_CODEX_LOGIN_LABEL, + OPENAI_CHATGPT_DEVICE_PAIRING_HINT, + OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, + OPENAI_CHATGPT_LOGIN_HINT, + OPENAI_CHATGPT_LOGIN_LABEL, OPENAI_CODEX_WIZARD_GROUP, } from "./auth-choice-copy.js"; import { @@ -41,7 +34,6 @@ import { } from "./base-url.js"; import { OPENAI_CODEX_DEFAULT_MODEL } from "./default-models.js"; import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js"; -import { buildOpenAICodexProvider } from "./openai-codex-catalog.js"; import { loginOpenAICodexDeviceCode } from "./openai-codex-device-code.js"; import { loginOpenAICodexOAuth } from "./openai-codex-oauth.runtime.js"; import { @@ -53,8 +45,8 @@ import { } from "./shared.js"; import { resolveOpenAICodexThinkingProfile } from "./thinking-policy.js"; -const PROVIDER_ID = "openai-codex"; -const OPENAI_PROVIDER_ID = "openai"; +const PROVIDER_ID = "openai"; +const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex"; const OPENAI_CODEX_BASE_URL = OPENAI_CODEX_RESPONSES_BASE_URL; const OPENAI_CODEX_LOGIN_ASSISTANT_PRIORITY = -30; const OPENAI_CODEX_DEVICE_PAIRING_ASSISTANT_PRIORITY = -10; @@ -115,6 +107,11 @@ const OPENAI_CODEX_MODERN_MODEL_IDS = [ OPENAI_CODEX_GPT_54_MINI_MODEL_ID, ] as const; +function isOpenAIOrLegacyCodexProvider(provider: string | undefined): boolean { + const normalized = normalizeProviderId(provider ?? ""); + return normalized === PROVIDER_ID || normalized === LEGACY_OPENAI_CODEX_PROVIDER_ID; +} + function isLegacyCodexCompatBaseUrl(baseUrl?: string): boolean { const trimmed = baseUrl?.trim(); return !!trimmed && /^https?:\/\/api\.githubcopilot\.com(?:\/v1)?\/?$/iu.test(trimmed); @@ -221,7 +218,7 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext) | undefined; return ( withDefaultCodexContextMetadata({ - model, + model: withCodexTransport(model, synthBaseUrl), contextWindow: OPENAI_CODEX_GPT_55_CODEX_CONTEXT_TOKENS, contextTokens: OPENAI_CODEX_GPT_55_DEFAULT_RUNTIME_CONTEXT_TOKENS, }) ?? @@ -281,6 +278,11 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext) } else { return undefined; } + patch = { + ...patch, + api: "openai-codex-responses", + baseUrl: synthBaseUrl, + }; return ( cloneFirstTemplateModel({ @@ -340,6 +342,20 @@ function withDefaultCodexContextMetadata(params: { }; } +function withCodexTransport( + model: ProviderRuntimeModel | undefined, + baseUrl: string, +): ProviderRuntimeModel | undefined { + if (!model) { + return undefined; + } + return normalizeModelCompat({ + ...model, + api: "openai-codex-responses", + baseUrl, + } as ProviderRuntimeModel); +} + function buildCodexCredentialExtra(identity: { accountId?: string; chatgptPlanType?: string; @@ -363,24 +379,6 @@ function buildOpenAICodexAuthConfigPatch(): NonNullable await runOpenAICodexOAuth(ctx), - }, - { - id: "device-code", - label: OPENAI_CODEX_DEVICE_PAIRING_LABEL, - hint: OPENAI_CODEX_DEVICE_PAIRING_HINT, - kind: "device_code", - wizard: { - choiceId: "openai-codex-device-code", - choiceLabel: OPENAI_CODEX_DEVICE_PAIRING_LABEL, - choiceHint: OPENAI_CODEX_DEVICE_PAIRING_HINT, - assistantPriority: OPENAI_CODEX_DEVICE_PAIRING_ASSISTANT_PRIORITY, - ...OPENAI_CODEX_WIZARD_GROUP, - }, - run: async (ctx) => await runOpenAICodexDeviceCode(ctx), - }, - createProviderApiKeyAuthMethod({ - providerId: OPENAI_PROVIDER_ID, - methodId: "api-key", - label: OPENAI_CODEX_API_KEY_BACKUP_LABEL, - hint: OPENAI_CODEX_API_KEY_BACKUP_HINT, - optionKey: "openaiApiKey", - flagName: "--openai-api-key", - envVar: "OPENAI_API_KEY", - promptMessage: "Enter OpenAI API key", - profileId: "openai:default", - defaultModel: OPENAI_CODEX_DEFAULT_MODEL, - expectedProviders: [OPENAI_PROVIDER_ID], - applyConfig: applyOpenAICodexAuthConfig, - wizard: { - choiceId: "openai-codex-api-key", - choiceLabel: OPENAI_CODEX_API_KEY_BACKUP_LABEL, - choiceHint: OPENAI_CODEX_API_KEY_BACKUP_HINT, - assistantPriority: 5, - assistantVisibility: "manual-only", - ...OPENAI_CODEX_WIZARD_GROUP, - }, - }), - ], - catalog: { - order: "profile", - run: async (ctx) => { - const authStore = ensureAuthProfileStoreForLocalUpdate(ctx.agentDir); - if (listProfilesForProvider(authStore, PROVIDER_ID).length === 0) { - return null; - } - return { - provider: buildOpenAICodexProvider(), - }; +export function buildOpenAIChatGPTAuthMethods(): ProviderAuthMethod[] { + return [ + { + id: "oauth", + label: OPENAI_CHATGPT_LOGIN_LABEL, + hint: OPENAI_CHATGPT_LOGIN_HINT, + kind: "oauth", + wizard: { + choiceId: "openai", + choiceLabel: OPENAI_CHATGPT_LOGIN_LABEL, + choiceHint: OPENAI_CHATGPT_LOGIN_HINT, + assistantPriority: OPENAI_CODEX_LOGIN_ASSISTANT_PRIORITY, + onboardingFeatured: true, + ...OPENAI_CODEX_WIZARD_GROUP, }, + run: async (ctx) => await runOpenAICodexOAuth(ctx), }, + { + id: "device-code", + label: OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, + hint: OPENAI_CHATGPT_DEVICE_PAIRING_HINT, + kind: "device_code", + wizard: { + choiceId: "openai-device-code", + choiceLabel: OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, + choiceHint: OPENAI_CHATGPT_DEVICE_PAIRING_HINT, + assistantPriority: OPENAI_CODEX_DEVICE_PAIRING_ASSISTANT_PRIORITY, + ...OPENAI_CODEX_WIZARD_GROUP, + }, + run: async (ctx) => await runOpenAICodexDeviceCode(ctx), + }, + ]; +} + +export function buildOpenAICodexProviderHooks(): Pick< + ProviderPlugin, + | "resolveDynamicModel" + | "buildAuthDoctorHint" + | "resolveThinkingProfile" + | "isModernModelRef" + | "preferRuntimeResolvedModel" + | "normalizeResolvedModel" + | "normalizeTransport" + | "resolveUsageAuth" + | "fetchUsageSnapshot" + | "refreshOAuth" + | "augmentModelCatalog" + | "resolveReasoningOutputMode" +> { + return { resolveDynamicModel: (ctx) => resolveCodexForwardCompatModel(ctx), buildAuthDoctorHint: (ctx) => buildOpenAICodexAuthDoctorHint(ctx), resolveThinkingProfile: ({ modelId }) => resolveOpenAICodexThinkingProfile(modelId), isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_CODEX_MODERN_MODEL_IDS), preferRuntimeResolvedModel: (ctx) => { - if (normalizeProviderId(ctx.provider) !== PROVIDER_ID) { + if (!isOpenAIOrLegacyCodexProvider(ctx.provider)) { return false; } const id = ctx.modelId.trim().toLowerCase(); @@ -612,7 +584,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { ...buildOpenAIResponsesProviderHooks(), resolveReasoningOutputMode: () => "native", normalizeResolvedModel: (ctx) => { - if (normalizeProviderId(ctx.provider) !== PROVIDER_ID) { + if (!isOpenAIOrLegacyCodexProvider(ctx.provider)) { return undefined; } const transportNormalized = normalizeCodexTransport(ctx.model); @@ -624,7 +596,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { return imageCapable === ctx.model ? undefined : imageCapable; }, normalizeTransport: ({ provider, api, baseUrl }) => { - if (normalizeProviderId(provider) !== PROVIDER_ID) { + if (!isOpenAIOrLegacyCodexProvider(provider)) { return undefined; } const normalized = normalizeCodexTransportFields({ api, baseUrl }); diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index bb6ebbdeec6d..c5bd568ce13c 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -1,7 +1,6 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import type { Context, Model, SimpleStreamOptions } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js"; import { buildOpenAIProvider } from "./openai-provider.js"; const mocks = vi.hoisted(() => ({ @@ -127,6 +126,8 @@ describe("buildOpenAIProvider", () => { const provider = buildOpenAIProvider(); const apiKey = provider.auth.find((method) => method.id === "api-key"); + expect(provider.hookAliases).toContain("openai-codex"); + expect(provider.catalog).toBeUndefined(); expectFields(apiKey?.wizard, { choiceLabel: "OpenAI API Key", choiceHint: "Use your OpenAI API key directly", @@ -136,6 +137,59 @@ describe("buildOpenAIProvider", () => { }); }); + it("keeps the deprecated Codex provider builder on the public API barrel", async () => { + const { buildOpenAICodexProviderPlugin } = await import("./api.js"); + const provider = buildOpenAICodexProviderPlugin(); + + expect(provider.id).toBe("openai"); + expect(provider.hookAliases).toContain("openai-codex"); + }); + + it("prefers auth-aware Codex runtime metadata over static OpenAI catalog rows", () => { + const provider = buildOpenAIProvider(); + + expect( + provider.preferRuntimeResolvedModel?.({ + provider: "openai", + modelId: "gpt-5.5", + } as never), + ).toBe(true); + }); + + it("normalizes legacy OpenAI Codex hook aliases through the Codex transport", () => { + const provider = buildOpenAIProvider(); + + expect( + provider.normalizeTransport?.({ + provider: "openai-codex", + api: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api", + } as never), + ).toEqual({ + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + }); + expect( + provider.normalizeResolvedModel?.({ + provider: "openai-codex", + modelId: "gpt-5.4", + model: { + provider: "openai-codex", + id: "gpt-5.4-codex", + name: "gpt-5.4-codex", + api: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api", + }, + } as never), + ).toMatchObject({ + id: "gpt-5.4", + name: "gpt-5.4", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + input: ["text", "image"], + }); + }); + it("resolves gpt-5.4 mini and nano from GPT-5 small-model templates", () => { const provider = buildOpenAIProvider(); const registry = { @@ -269,19 +323,41 @@ describe("buildOpenAIProvider", () => { ).toBe("native"); }); - it("keeps GPT-5.4 family metadata aligned with native OpenAI docs", () => { + it("routes GPT forward-compat models by selected OpenAI auth mode", () => { const provider = buildOpenAIProvider(); - const codexProvider = buildOpenAICodexProviderPlugin(); const openaiModel = provider.resolveDynamicModel?.({ provider: "openai", modelId: "gpt-5.4", modelRegistry: { find: () => null }, + providerConfig: { + auth: "api-key", + }, } as never); - const codexModel = codexProvider.resolveDynamicModel?.({ - provider: "openai-codex", + const codexModel = provider.resolveDynamicModel?.({ + provider: "openai", modelId: "gpt-5.4", modelRegistry: { find: () => null }, + config: { + auth: { + profiles: { + "openai:default": { + provider: "openai", + mode: "oauth", + }, + }, + order: { + openai: ["openai:default"], + }, + }, + }, + } as never); + const selectedOauthModel = provider.resolveDynamicModel?.({ + provider: "openai", + modelId: "gpt-5.4", + modelRegistry: { find: () => null }, + authProfileId: "openai:work", + authProfileMode: "oauth", } as never); expectFields(openaiModel, { @@ -293,7 +369,15 @@ describe("buildOpenAIProvider", () => { maxTokens: 128_000, }); expectFields(codexModel, { - provider: "openai-codex", + provider: "openai", + id: "gpt-5.4", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + contextWindow: 1_050_000, + maxTokens: 128_000, + }); + expectFields(selectedOauthModel, { + provider: "openai", id: "gpt-5.4", api: "openai-codex-responses", baseUrl: "https://chatgpt.com/backend-api/codex", @@ -432,6 +516,27 @@ describe("buildOpenAIProvider", () => { }); }); + it("keeps legacy OpenAI Codex refs on the Codex thinking policy", () => { + const provider = buildOpenAIProvider(); + + expect( + provider + .resolveThinkingProfile?.({ + provider: "openai-codex", + modelId: "gpt-5.3-codex-spark", + } as never) + ?.levels.map((level) => level.id), + ).toContain("xhigh"); + expect( + provider + .resolveThinkingProfile?.({ + provider: "openai", + modelId: "gpt-5.3-codex-spark", + } as never) + ?.levels.map((level) => level.id), + ).not.toContain("xhigh"); + }); + it("keeps chat-latest and gpt-5.5 out of synthetic catalog metadata", () => { const provider = buildOpenAIProvider(); @@ -455,7 +560,7 @@ describe("buildOpenAIProvider", () => { it("keeps modern live selection on current OpenAI and Codex models", () => { const provider = buildOpenAIProvider(); - const codexProvider = buildOpenAICodexProviderPlugin(); + const codexProvider = buildOpenAIProvider(); expect( provider.isModernModelRef?.({ @@ -490,31 +595,31 @@ describe("buildOpenAIProvider", () => { expect( codexProvider.isModernModelRef?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.1-codex", } as never), ).toBe(false); expect( codexProvider.isModernModelRef?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.1-codex-max", } as never), ).toBe(false); expect( codexProvider.isModernModelRef?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.2-codex", } as never), ).toBe(false); expect( codexProvider.isModernModelRef?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.4", } as never), ).toBe(true); expect( codexProvider.isModernModelRef?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.5", } as never), ).toBe(true); @@ -522,7 +627,7 @@ describe("buildOpenAIProvider", () => { it("owns replay policy for OpenAI and Codex transports", () => { const provider = buildOpenAIProvider(); - const codexProvider = buildOpenAICodexProviderPlugin(); + const codexProvider = buildOpenAIProvider(); expect( provider.buildReplayPolicy?.({ @@ -555,7 +660,7 @@ describe("buildOpenAIProvider", () => { expect( codexProvider.buildReplayPolicy?.({ - provider: "openai-codex", + provider: "openai", modelApi: "openai-codex-responses", modelId: "gpt-5.4", } as never), @@ -789,25 +894,51 @@ describe("buildOpenAIProvider", () => { }); it("defaults Codex responses transport without forcing extra flags", () => { - const provider = buildOpenAICodexProviderPlugin(); + const provider = buildOpenAIProvider(); expect( provider.prepareExtraParams?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.4", extraParams: { effort: "high" }, + config: { + auth: { + profiles: { + "openai:default": { + provider: "openai", + mode: "oauth", + }, + }, + }, + }, } as never), ).toEqual({ effort: "high", transport: "auto", }); + expect( + provider.prepareExtraParams?.({ + provider: "openai", + modelId: "gpt-5.4", + model: { + api: "openai-codex-responses", + provider: "openai", + id: "gpt-5.4", + baseUrl: "https://chatgpt.com/backend-api/codex/responses", + } as Model<"openai-codex-responses">, + extraParams: { effort: "high" }, + }), + ).toEqual({ + effort: "high", + transport: "auto", + }); const explicit = { transport: "sse", }; expect( provider.prepareExtraParams?.({ - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.4", extraParams: explicit, } as never), @@ -816,7 +947,7 @@ describe("buildOpenAIProvider", () => { it("shares OpenAI responses wrapper composition across provider variants", () => { const provider = buildOpenAIProvider(); - const codexProvider = buildOpenAICodexProviderPlugin(); + const codexProvider = buildOpenAIProvider(); expect(provider.wrapStreamFn).toBe(codexProvider.wrapStreamFn); expect(provider.buildReplayPolicy).toBe(codexProvider.buildReplayPolicy); @@ -853,7 +984,7 @@ describe("buildOpenAIProvider", () => { }); it("owns Codex wrapper composition for responses payloads", () => { - const provider = buildOpenAICodexProviderPlugin(); + const provider = buildOpenAIProvider(); const wrap = provider.wrapStreamFn; expect(wrap).toBeTypeOf("function"); if (!wrap) { @@ -870,7 +1001,7 @@ describe("buildOpenAIProvider", () => { }); const result = runWrappedPayloadCase({ wrap, - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.4", extraParams: { fastMode: true, @@ -880,8 +1011,8 @@ describe("buildOpenAIProvider", () => { cfg: { auth: { profiles: { - "openai-codex:default": { - provider: "openai-codex", + "openai:default": { + provider: "openai", mode: "oauth", }, }, @@ -901,7 +1032,7 @@ describe("buildOpenAIProvider", () => { }, model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.4", baseUrl: "https://chatgpt.com/backend-api", } as Model<"openai-codex-responses">, @@ -928,10 +1059,10 @@ describe("buildOpenAIProvider", () => { ]); }); it("falls back to cached codex oauth credentials on accountId extraction failures", async () => { - const provider = buildOpenAICodexProviderPlugin(); + const provider = buildOpenAIProvider(); const credential = { type: "oauth" as const, - provider: "openai-codex", + provider: "openai", access: "cached-access-token", refresh: "refresh-token", expires: Date.now() - 60_000, diff --git a/extensions/openai/openai-provider.ts b/extensions/openai/openai-provider.ts index ac8436785ef3..279cdc4c95a8 100644 --- a/extensions/openai/openai-provider.ts +++ b/extensions/openai/openai-provider.ts @@ -11,8 +11,12 @@ import { } from "openclaw/plugin-sdk/provider-model-shared"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { OPENAI_ACCOUNT_WIZARD_GROUP, OPENAI_API_KEY_LABEL } from "./auth-choice-copy.js"; -import { isOpenAIApiBaseUrl } from "./base-url.js"; +import { isOpenAIApiBaseUrl, isOpenAICodexBaseUrl } from "./base-url.js"; import { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "./default-models.js"; +import { + buildOpenAIChatGPTAuthMethods, + buildOpenAICodexProviderHooks, +} from "./openai-codex-provider.js"; import { buildOpenAIResponsesProviderHooks, buildOpenAISyntheticCatalogEntry, @@ -20,9 +24,13 @@ import { findCatalogTemplate, matchesExactOrPrefix, } from "./shared.js"; -import { resolveOpenAIThinkingProfile } from "./thinking-policy.js"; +import { + resolveOpenAICodexThinkingProfile, + resolveOpenAIThinkingProfile, +} from "./thinking-policy.js"; const PROVIDER_ID = "openai"; +const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex"; const OPENAI_CHAT_LATEST_MODEL_ID = "chat-latest"; const OPENAI_GPT_55_MODEL_ID = "gpt-5.5"; const OPENAI_GPT_55_PRO_MODEL_ID = "gpt-5.5-pro"; @@ -95,6 +103,11 @@ function shouldUseOpenAIResponsesTransport(params: { return typeof params.baseUrl === "string" && isOpenAIApiBaseUrl(params.baseUrl); } +function isOpenAIOrLegacyCodexProvider(provider: string | undefined): boolean { + const normalized = normalizeProviderId(provider ?? ""); + return normalized === PROVIDER_ID || normalized === LEGACY_OPENAI_CODEX_PROVIDER_ID; +} + function normalizeOpenAITransport(model: ProviderRuntimeModel): ProviderRuntimeModel { const useResponsesTransport = shouldUseOpenAIResponsesTransport({ provider: model.provider, @@ -112,6 +125,95 @@ function normalizeOpenAITransport(model: ProviderRuntimeModel): ProviderRuntimeM }; } +function shouldUseCodexResponsesHooks(params: { + provider?: string; + api?: ProviderRuntimeModel["api"] | null; + baseUrl?: string; +}): boolean { + if (normalizeProviderId(params.provider ?? "") === LEGACY_OPENAI_CODEX_PROVIDER_ID) { + return true; + } + if (params.api === "openai-codex-responses") { + return true; + } + return typeof params.baseUrl === "string" && isOpenAICodexBaseUrl(params.baseUrl); +} + +function resolveConfiguredAuthTransport( + ctx: Pick< + ProviderResolveDynamicModelContext, + "authProfileId" | "authProfileMode" | "config" | "providerConfig" + >, +) { + if (ctx.authProfileMode === "oauth" || ctx.authProfileMode === "token") { + return "codex"; + } + if (ctx.authProfileMode === "api_key" || ctx.authProfileMode === "aws-sdk") { + return "responses"; + } + if (ctx.authProfileId && isLegacyOpenAIProfileId(ctx.authProfileId)) { + return "codex"; + } + const authMode = ctx.providerConfig?.auth; + if (authMode === "oauth" || authMode === "token") { + return "codex"; + } + if (authMode === "api-key") { + return "responses"; + } + + const auth = ctx.config?.auth; + const profiles = auth?.profiles ?? {}; + const orderedProfileIds = [ + ...(auth?.order?.[PROVIDER_ID] ?? []), + ...(auth?.order?.[LEGACY_OPENAI_CODEX_PROVIDER_ID] ?? []), + ]; + for (const profileId of orderedProfileIds) { + const mode = profiles[profileId]?.mode; + if (mode === "oauth" || mode === "token") { + return "codex"; + } + if (mode === "api_key") { + return "responses"; + } + } + + const providerModes = Object.values(profiles) + .filter((profile) => + [PROVIDER_ID, LEGACY_OPENAI_CODEX_PROVIDER_ID].includes( + normalizeProviderId(profile.provider), + ), + ) + .map((profile) => profile.mode); + if (providerModes.some((mode) => mode === "oauth" || mode === "token")) { + return "codex"; + } + if (providerModes.includes("api_key")) { + return "responses"; + } + return undefined; +} + +function isLegacyOpenAIProfileId(profileId: string): boolean { + return normalizeProviderId(profileId.split(":", 1)[0] ?? "") === LEGACY_OPENAI_CODEX_PROVIDER_ID; +} + +function shouldResolveDynamicModelThroughCodex(ctx: ProviderResolveDynamicModelContext): boolean { + if ( + shouldUseCodexResponsesHooks({ + provider: ctx.provider, + api: ctx.providerConfig?.api, + baseUrl: ctx.providerConfig?.baseUrl, + }) + ) { + return true; + } + if (ctx.providerConfig?.baseUrl && !isOpenAIApiBaseUrl(ctx.providerConfig.baseUrl)) { + return false; + } + return resolveConfiguredAuthTransport(ctx) === "codex"; +} + function resolveOpenAIGptForwardCompatModel(ctx: ProviderResolveDynamicModelContext) { const trimmedModelId = ctx.modelId.trim(); const lower = normalizeLowercaseStringOrEmpty(trimmedModelId); @@ -227,13 +329,23 @@ function resolveOpenAIGptForwardCompatModel(ctx: ProviderResolveDynamicModelCont } export function buildOpenAIProvider(): ProviderPlugin { + const codexHooks = buildOpenAICodexProviderHooks(); + const codexResponsesHooks = buildOpenAIResponsesProviderHooks(); + const responsesHooks = buildOpenAIResponsesProviderHooks({ transport: "sse" }); return { id: PROVIDER_ID, label: "OpenAI", - hookAliases: ["azure-openai", "azure-openai-responses"], + hookAliases: ["openai-codex", "azure-openai", "azure-openai-responses"], docsPath: "/providers/models", envVars: ["OPENAI_API_KEY"], + oauthProfileIdRepairs: [ + { + legacyProfileId: "openai-codex:default", + promptLabel: "OpenAI", + }, + ], auth: [ + ...buildOpenAIChatGPTAuthMethods(), createProviderApiKeyAuthMethod({ providerId: PROVIDER_ID, methodId: "api-key", @@ -243,6 +355,7 @@ export function buildOpenAIProvider(): ProviderPlugin { flagName: "--openai-api-key", envVar: "OPENAI_API_KEY", promptMessage: "Enter OpenAI API key", + profileId: "openai:api-key", defaultModel: OPENAI_DEFAULT_MODEL, expectedProviders: ["openai"], applyConfig: (cfg) => applyOpenAIConfig(cfg), @@ -255,22 +368,61 @@ export function buildOpenAIProvider(): ProviderPlugin { }, }), ], - resolveDynamicModel: (ctx) => resolveOpenAIGptForwardCompatModel(ctx), + resolveDynamicModel: (ctx) => + shouldResolveDynamicModelThroughCodex(ctx) + ? codexHooks.resolveDynamicModel?.(ctx) + : resolveOpenAIGptForwardCompatModel(ctx), + preferRuntimeResolvedModel: (ctx) => codexHooks.preferRuntimeResolvedModel?.(ctx) ?? false, normalizeResolvedModel: (ctx) => { - if (normalizeProviderId(ctx.provider) !== PROVIDER_ID) { + if (!isOpenAIOrLegacyCodexProvider(ctx.provider)) { return undefined; } + if ( + shouldUseCodexResponsesHooks({ + provider: ctx.provider, + api: ctx.model.api, + baseUrl: ctx.model.baseUrl, + }) + ) { + return codexHooks.normalizeResolvedModel?.(ctx); + } return normalizeOpenAITransport(ctx.model); }, - normalizeTransport: ({ provider, api, baseUrl }) => - shouldUseOpenAIResponsesTransport({ provider, api, baseUrl }) - ? { api: "openai-responses", baseUrl } - : undefined, - ...buildOpenAIResponsesProviderHooks({ transport: "sse" }), + normalizeTransport: (ctx) => { + if (shouldUseCodexResponsesHooks(ctx)) { + return codexHooks.normalizeTransport?.(ctx); + } + return shouldUseOpenAIResponsesTransport(ctx) + ? { api: "openai-responses", baseUrl: ctx.baseUrl } + : undefined; + }, + ...responsesHooks, + prepareExtraParams: (ctx) => { + const providerConfig = ctx.config?.models?.providers?.[PROVIDER_ID]; + const useCodexTransport = + shouldUseCodexResponsesHooks({ + provider: ctx.provider, + api: ctx.model?.api, + baseUrl: ctx.model?.baseUrl, + }) || + (normalizeProviderId(ctx.provider) === PROVIDER_ID && + (!providerConfig?.baseUrl || isOpenAIApiBaseUrl(providerConfig.baseUrl)) && + resolveConfiguredAuthTransport({ + config: ctx.config, + providerConfig, + }) === "codex"); + return (useCodexTransport ? codexResponsesHooks : responsesHooks).prepareExtraParams?.(ctx); + }, + resolveUsageAuth: codexHooks.resolveUsageAuth, + fetchUsageSnapshot: codexHooks.fetchUsageSnapshot, + refreshOAuth: codexHooks.refreshOAuth, matchesContextOverflowError: ({ errorMessage }) => /content_filter.*(?:prompt|input).*(?:too long|exceed)/i.test(errorMessage), resolveReasoningOutputMode: () => "native", - resolveThinkingProfile: ({ modelId }) => resolveOpenAIThinkingProfile(modelId), + resolveThinkingProfile: ({ provider, modelId }) => + normalizeProviderId(provider) === LEGACY_OPENAI_CODEX_PROVIDER_ID + ? resolveOpenAICodexThinkingProfile(modelId) + : resolveOpenAIThinkingProfile(modelId), isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_MODERN_MODEL_IDS), buildMissingAuthMessage: (ctx) => { if (ctx.provider !== PROVIDER_ID || ctx.listProfileIds("openai-codex").length === 0) { @@ -339,3 +491,8 @@ export function buildOpenAIProvider(): ProviderPlugin { }, }; } + +/** @deprecated Use buildOpenAIProvider; OpenAI Codex is now an OpenAI auth/transport mode. */ +export function buildOpenAICodexProviderPlugin(): ProviderPlugin { + return buildOpenAIProvider(); +} diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index 3f24552e01a0..23415355fbf8 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -4,21 +4,12 @@ "onStartup": false }, "enabledByDefault": true, - "providers": ["openai", "openai-codex"], + "providers": ["openai"], "modelSupport": { "modelPrefixes": ["gpt-", "o1", "o3", "o4"] }, - "modelPricing": { - "providers": { - "openai-codex": { - "openRouter": { - "provider": "openai" - }, - "liteLLM": { - "provider": "openai" - } - } - } + "providerAuthAliases": { + "openai-codex": "openai" }, "providerEndpoints": [ { @@ -38,9 +29,6 @@ "providers": { "openai": { "family": "openai-family" - }, - "openai-codex": { - "family": "openai-family" } } }, @@ -214,77 +202,6 @@ "cost": { "input": 30, "output": 180, "cacheRead": 0, "cacheWrite": 0 } } ] - }, - "openai-codex": { - "baseUrl": "https://chatgpt.com/backend-api/codex", - "api": "openai-codex-responses", - "models": [ - { - "id": "gpt-5.5", - "name": "gpt-5.5", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 6000, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 400000, - "contextTokens": 272000, - "maxTokens": 128000, - "cost": { "input": 5, "output": 30, "cacheRead": 0.5, "cacheWrite": 0 } - }, - { - "id": "gpt-5.4", - "name": "gpt-5.4", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2048, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 1050000, - "contextTokens": 272000, - "maxTokens": 128000, - "cost": { "input": 2.5, "output": 15, "cacheRead": 0.25, "cacheWrite": 0 } - }, - { - "id": "gpt-5.4-pro", - "name": "gpt-5.4-pro", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2048, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 1050000, - "contextTokens": 272000, - "maxTokens": 128000, - "cost": { "input": 30, "output": 180, "cacheRead": 0, "cacheWrite": 0 } - }, - { - "id": "gpt-5.4-mini", - "name": "gpt-5.4-mini", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 2048, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 400000, - "contextTokens": 272000, - "maxTokens": 128000, - "cost": { "input": 0.75, "output": 4.5, "cacheRead": 0.075, "cacheWrite": 0 } - }, - { - "id": "gpt-5.5-pro", - "name": "gpt-5.5-pro", - "reasoning": true, - "input": ["text", "image"], - "mediaInput": { - "image": { "maxSidePx": 6000, "preferredSidePx": 2048, "tokenMode": "detail" } - }, - "contextWindow": 1000000, - "contextTokens": 272000, - "maxTokens": 128000, - "cost": { "input": 30, "output": 180, "cacheRead": 0, "cacheWrite": 0 } - } - ] } }, "aliases": { @@ -294,19 +211,18 @@ } }, "discovery": { - "openai": "static", - "openai-codex": "refreshable" + "openai": "static" }, "suppressions": [ { "provider": "openai", "model": "gpt-5.3-codex-spark", - "reason": "gpt-5.3-codex-spark is not exposed by the OpenAI API catalog. Use openai-codex/gpt-5.3-codex-spark or openai/gpt-5.5." + "reason": "gpt-5.3-codex-spark is not exposed by the OpenAI API catalog. Use openai/gpt-5.5." }, { "provider": "azure-openai-responses", "model": "gpt-5.3-codex-spark", - "reason": "gpt-5.3-codex-spark is not exposed by the OpenAI API catalog. Use openai-codex/gpt-5.3-codex-spark or openai/gpt-5.5." + "reason": "gpt-5.3-codex-spark is not exposed by the OpenAI API catalog. Use openai/gpt-5.5." } ] }, @@ -323,18 +239,20 @@ "provider": "openai", "method": "oauth", "choiceId": "openai", + "deprecatedChoiceIds": ["openai-codex", "codex-cli", "openai-codex-import"], "choiceLabel": "ChatGPT Login", "choiceHint": "Sign in with your ChatGPT or Codex subscription", "assistantPriority": -40, - "assistantVisibility": "manual-only", "groupId": "openai", "groupLabel": "OpenAI", - "groupHint": "ChatGPT/Codex sign-in or API key" + "groupHint": "ChatGPT/Codex sign-in or API key", + "onboardingFeatured": true }, { "provider": "openai", "method": "device-code", "choiceId": "openai-device-code", + "deprecatedChoiceIds": ["openai-codex-device-code"], "choiceLabel": "ChatGPT Device Pairing", "choiceHint": "Pair your ChatGPT account in browser with a device code", "assistantPriority": -10, @@ -343,50 +261,11 @@ "groupLabel": "OpenAI", "groupHint": "ChatGPT/Codex sign-in or API key" }, - { - "provider": "openai-codex", - "method": "oauth", - "choiceId": "openai-codex", - "deprecatedChoiceIds": ["codex-cli", "openai-codex-import"], - "choiceLabel": "ChatGPT/Codex Browser Login", - "choiceHint": "Sign in with OpenAI in your browser", - "assistantPriority": -30, - "groupId": "openai", - "groupLabel": "OpenAI", - "groupHint": "ChatGPT/Codex sign-in or API key", - "onboardingFeatured": true - }, - { - "provider": "openai-codex", - "method": "device-code", - "choiceId": "openai-codex-device-code", - "choiceLabel": "ChatGPT/Codex Device Pairing", - "choiceHint": "Pair in browser with a device code", - "assistantPriority": -10, - "groupId": "openai", - "groupLabel": "OpenAI", - "groupHint": "ChatGPT/Codex sign-in or API key" - }, - { - "provider": "openai-codex", - "method": "api-key", - "choiceId": "openai-codex-api-key", - "choiceLabel": "OpenAI API Key Backup", - "choiceHint": "Use an OpenAI API key when your Codex subscription is unavailable", - "assistantPriority": 5, - "assistantVisibility": "manual-only", - "groupId": "openai", - "groupLabel": "OpenAI", - "groupHint": "ChatGPT/Codex sign-in or API key", - "optionKey": "openaiApiKey", - "cliFlag": "--openai-api-key", - "cliOption": "--openai-api-key ", - "cliDescription": "OpenAI API Key Backup" - }, { "provider": "openai", "method": "api-key", "choiceId": "openai-api-key", + "deprecatedChoiceIds": ["openai-codex-api-key"], "choiceLabel": "OpenAI API Key", "choiceHint": "Use your OpenAI API key directly", "assistantPriority": 5, @@ -405,31 +284,21 @@ "realtimeTranscriptionProviders": ["openai"], "realtimeVoiceProviders": ["openai"], "memoryEmbeddingProviders": ["openai"], - "mediaUnderstandingProviders": ["openai", "openai-codex"], + "mediaUnderstandingProviders": ["openai"], "imageGenerationProviders": ["openai"], "videoGenerationProviders": ["openai"] }, "imageGenerationProviderMetadata": { "openai": { - "aliases": ["openai-codex"], "authSignals": [ { "provider": "openai" - }, - { - "provider": "openai-codex", - "providerBaseUrl": { - "provider": "openai", - "defaultBaseUrl": "https://api.openai.com/v1", - "allowedBaseUrls": ["https://api.openai.com/v1"] - } } ] } }, "videoGenerationProviderMetadata": { "openai": { - "aliases": ["openai-codex"], "authSignals": [ { "provider": "openai" @@ -439,17 +308,6 @@ }, "mediaUnderstandingProviderMetadata": { "openai": { - "capabilities": ["image", "audio"], - "defaultModels": { - "image": "gpt-5.4-mini", - "audio": "gpt-4o-transcribe" - }, - "autoPriority": { - "image": 10, - "audio": 10 - } - }, - "openai-codex": { "capabilities": ["image", "audio"], "defaultModels": { "image": "gpt-5.5", diff --git a/extensions/openai/openclaw.plugin.test.ts b/extensions/openai/openclaw.plugin.test.ts index 32263108539a..8003ad690923 100644 --- a/extensions/openai/openclaw.plugin.test.ts +++ b/extensions/openai/openclaw.plugin.test.ts @@ -1,8 +1,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js"; import { buildOpenAIProvider } from "./openai-provider.js"; -import { buildOpenAICodexSetupProvider, buildOpenAISetupProvider } from "./setup-api.js"; +import { buildOpenAISetupProvider } from "./setup-api.js"; const manifest = JSON.parse( readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"), @@ -27,6 +26,10 @@ const manifest = JSON.parse( groupLabel?: string; groupHint?: string; }>; + setup?: { + providers?: Array<{ id: string }>; + }; + providerAuthAliases?: Record; }; const packageJson = JSON.parse( @@ -59,12 +62,7 @@ function manifestComparableWizardFields(choice: { } function providerWizardByKey() { - const providers = [ - buildOpenAIProvider(), - buildOpenAICodexProviderPlugin(), - buildOpenAISetupProvider(), - buildOpenAICodexSetupProvider(), - ]; + const providers = [buildOpenAIProvider(), buildOpenAISetupProvider()]; const wizards = new Map>(); for (const provider of providers) { @@ -97,16 +95,21 @@ describe("OpenAI plugin manifest", () => { expect(packageJson.dependencies?.ws).toBe("8.21.0"); }); - it("keeps removed Codex CLI import auth choice as a deprecated browser-login alias", () => { - const codexBrowserLogin = manifest.providerAuthChoices?.find( - (choice) => choice.choiceId === "openai-codex", + it("keeps removed Codex CLI import auth choice as a deprecated OpenAI login alias", () => { + const openAiLogin = manifest.providerAuthChoices?.find( + (choice) => choice.choiceId === "openai", ); - expect(codexBrowserLogin?.deprecatedChoiceIds).toContain("openai-codex-import"); + expect(openAiLogin?.deprecatedChoiceIds).toContain("openai-codex-import"); }); - it("keeps Codex media-understanding manifest metadata aligned with runtime audio support", () => { - const metadata = manifest.mediaUnderstandingProviderMetadata?.["openai-codex"]; + it("keeps legacy OpenAI Codex setup lookup routed to the OpenAI setup runtime", () => { + expect(manifest.setup?.providers?.map((provider) => provider.id)).toEqual(["openai"]); + expect(manifest.providerAuthAliases?.["openai-codex"]).toBe("openai"); + }); + + it("keeps OpenAI media-understanding manifest metadata aligned with runtime audio support", () => { + const metadata = manifest.mediaUnderstandingProviderMetadata?.openai; expect(metadata?.capabilities).toEqual(["image", "audio"]); expect(metadata?.defaultModels?.image).toBe("gpt-5.5"); expect(metadata?.defaultModels?.audio).toBe("gpt-4o-transcribe"); @@ -116,20 +119,15 @@ describe("OpenAI plugin manifest", () => { it("labels OpenAI API key and Codex auth choices without stale mixed OAuth wording", () => { const choices = manifest.providerAuthChoices ?? []; - const codexBrowserLogin = choices.find((choice) => choice.choiceId === "openai-codex"); - const codexDeviceCode = choices.find( - (choice) => choice.choiceId === "openai-codex-device-code", - ); const openAiLogin = choices.find((choice) => choice.choiceId === "openai"); const openAiDeviceCode = choices.find((choice) => choice.choiceId === "openai-device-code"); const apiKey = choices.find( (choice) => choice.provider === "openai" && choice.method === "api-key", ); - const codexApiKey = choices.find((choice) => choice.choiceId === "openai-codex-api-key"); expect(openAiLogin?.choiceLabel).toBe("ChatGPT Login"); expect(openAiLogin?.choiceHint).toBe("Sign in with your ChatGPT or Codex subscription"); - expect(openAiLogin?.assistantVisibility).toBe("manual-only"); + expect(openAiLogin?.assistantVisibility).toBeUndefined(); expect(openAiLogin?.groupId).toBe("openai"); expect(openAiLogin?.groupLabel).toBe("OpenAI"); expect(openAiLogin?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); @@ -141,28 +139,11 @@ describe("OpenAI plugin manifest", () => { expect(openAiDeviceCode?.groupId).toBe("openai"); expect(openAiDeviceCode?.groupLabel).toBe("OpenAI"); expect(openAiDeviceCode?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); - expect(codexBrowserLogin?.choiceLabel).toBe("ChatGPT/Codex Browser Login"); - expect(codexBrowserLogin?.choiceHint).toBe("Sign in with OpenAI in your browser"); - expect(codexBrowserLogin?.groupId).toBe("openai"); - expect(codexBrowserLogin?.groupLabel).toBe("OpenAI"); - expect(codexBrowserLogin?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); - expect(codexDeviceCode?.choiceLabel).toBe("ChatGPT/Codex Device Pairing"); - expect(codexDeviceCode?.choiceHint).toBe("Pair in browser with a device code"); - expect(codexDeviceCode?.groupId).toBe("openai"); - expect(codexDeviceCode?.groupLabel).toBe("OpenAI"); - expect(codexDeviceCode?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); expect(apiKey?.choiceLabel).toBe("OpenAI API Key"); expect(apiKey?.choiceHint).toBe("Use your OpenAI API key directly"); expect(apiKey?.groupId).toBe("openai"); expect(apiKey?.groupLabel).toBe("OpenAI"); expect(apiKey?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); - expect(codexApiKey?.choiceLabel).toBe("OpenAI API Key Backup"); - expect(codexApiKey?.choiceHint).toBe( - "Use an OpenAI API key when your Codex subscription is unavailable", - ); - expect(codexApiKey?.groupId).toBe("openai"); - expect(codexApiKey?.groupLabel).toBe("OpenAI"); - expect(codexApiKey?.groupHint).toBe("ChatGPT/Codex sign-in or API key"); expect(choices.map((choice) => choice.choiceLabel)).not.toContain( "OpenAI Codex (ChatGPT OAuth)", ); diff --git a/extensions/openai/prompt-overlay.ts b/extensions/openai/prompt-overlay.ts index a5d5abff8479..06d8747d879f 100644 --- a/extensions/openai/prompt-overlay.ts +++ b/extensions/openai/prompt-overlay.ts @@ -8,7 +8,7 @@ import { type Gpt5PromptOverlayMode, } from "openclaw/plugin-sdk/provider-model-shared"; -const OPENAI_PROVIDER_IDS = new Set(["openai", "openai-codex"]); +const OPENAI_PROVIDER_IDS = new Set(["openai"]); export const OPENAI_FRIENDLY_PROMPT_OVERLAY = GPT5_FRIENDLY_CHAT_PROMPT_OVERLAY; export const OPENAI_HEARTBEAT_PROMPT_OVERLAY = GPT5_HEARTBEAT_PROMPT_OVERLAY; diff --git a/extensions/openai/provider-contract-api.ts b/extensions/openai/provider-contract-api.ts index 6b4f2e2395a1..ea2f46065969 100644 --- a/extensions/openai/provider-contract-api.ts +++ b/extensions/openai/provider-contract-api.ts @@ -2,68 +2,58 @@ import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared"; import { OPENAI_ACCOUNT_WIZARD_GROUP, OPENAI_API_KEY_LABEL, - OPENAI_CODEX_DEVICE_PAIRING_HINT, - OPENAI_CODEX_DEVICE_PAIRING_LABEL, - OPENAI_CODEX_LOGIN_HINT, - OPENAI_CODEX_LOGIN_LABEL, - OPENAI_CODEX_WIZARD_GROUP, + OPENAI_CHATGPT_DEVICE_PAIRING_HINT, + OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, + OPENAI_CHATGPT_LOGIN_HINT, + OPENAI_CHATGPT_LOGIN_LABEL, } from "./auth-choice-copy.js"; const noopAuth = async () => ({ profiles: [] }); -export function createOpenAICodexProvider(): ProviderPlugin { +export function createOpenAIProvider(): ProviderPlugin { return { - id: "openai-codex", - label: "OpenAI Codex", + id: "openai", + label: "OpenAI", + hookAliases: ["openai-codex", "azure-openai", "azure-openai-responses"], docsPath: "/providers/models", + envVars: ["OPENAI_API_KEY"], oauthProfileIdRepairs: [ { legacyProfileId: "openai-codex:default", - promptLabel: "OpenAI Codex", + promptLabel: "OpenAI", }, ], auth: [ { id: "oauth", kind: "oauth", - label: OPENAI_CODEX_LOGIN_LABEL, - hint: OPENAI_CODEX_LOGIN_HINT, + label: OPENAI_CHATGPT_LOGIN_LABEL, + hint: OPENAI_CHATGPT_LOGIN_HINT, run: noopAuth, wizard: { - choiceId: "openai-codex", - choiceLabel: OPENAI_CODEX_LOGIN_LABEL, - choiceHint: OPENAI_CODEX_LOGIN_HINT, - assistantPriority: -30, + choiceId: "openai", + choiceLabel: OPENAI_CHATGPT_LOGIN_LABEL, + choiceHint: OPENAI_CHATGPT_LOGIN_HINT, + assistantPriority: -40, onboardingFeatured: true, - ...OPENAI_CODEX_WIZARD_GROUP, + ...OPENAI_ACCOUNT_WIZARD_GROUP, }, }, { id: "device-code", kind: "device_code", - label: OPENAI_CODEX_DEVICE_PAIRING_LABEL, - hint: OPENAI_CODEX_DEVICE_PAIRING_HINT, + label: OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, + hint: OPENAI_CHATGPT_DEVICE_PAIRING_HINT, run: noopAuth, wizard: { - choiceId: "openai-codex-device-code", - choiceLabel: OPENAI_CODEX_DEVICE_PAIRING_LABEL, - choiceHint: OPENAI_CODEX_DEVICE_PAIRING_HINT, + choiceId: "openai-device-code", + choiceLabel: OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, + choiceHint: OPENAI_CHATGPT_DEVICE_PAIRING_HINT, assistantPriority: -10, - ...OPENAI_CODEX_WIZARD_GROUP, + assistantVisibility: "manual-only", + ...OPENAI_ACCOUNT_WIZARD_GROUP, }, }, - ], - }; -} - -export function createOpenAIProvider(): ProviderPlugin { - return { - id: "openai", - label: "OpenAI", - hookAliases: ["azure-openai", "azure-openai-responses"], - docsPath: "/providers/models", - envVars: ["OPENAI_API_KEY"], - auth: [ { id: "api-key", kind: "api_key", diff --git a/extensions/openai/provider-policy-api.test.ts b/extensions/openai/provider-policy-api.test.ts new file mode 100644 index 000000000000..21a90764dcf8 --- /dev/null +++ b/extensions/openai/provider-policy-api.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { resolveThinkingProfile } from "./provider-policy-api.js"; + +describe("OpenAI provider policy artifact", () => { + it("keeps legacy Codex thinking policy for openai-codex refs", () => { + const codexProfile = resolveThinkingProfile({ + provider: "openai-codex", + modelId: "gpt-5.3-codex-spark", + }); + const openaiProfile = resolveThinkingProfile({ + provider: "openai", + modelId: "gpt-5.3-codex-spark", + }); + + expect(codexProfile?.levels.map((level) => level.id)).toContain("xhigh"); + expect(openaiProfile?.levels.map((level) => level.id)).not.toContain("xhigh"); + }); +}); diff --git a/extensions/openai/realtime-transcription-provider.ts b/extensions/openai/realtime-transcription-provider.ts index d00b90479e80..5a2594fdc0d0 100644 --- a/extensions/openai/realtime-transcription-provider.ts +++ b/extensions/openai/realtime-transcription-provider.ts @@ -143,7 +143,7 @@ async function resolveOpenAIRealtimeTranscriptionAuthorization( return apiKey; } const authToken = await resolveProviderAuthProfileApiKey({ - provider: "openai-codex", + provider: "openai", cfg: config.cfg, }); if (!authToken) { @@ -261,7 +261,7 @@ export function buildOpenAIRealtimeTranscriptionProvider(): RealtimeTranscriptio Boolean( normalizeProviderConfig(providerConfig).apiKey || process.env.OPENAI_API_KEY || - isProviderAuthProfileConfigured({ provider: "openai-codex", cfg }), + isProviderAuthProfileConfigured({ provider: "openai", cfg }), ), createSession: (req) => { const config = normalizeProviderConfig(req.providerConfig); diff --git a/extensions/openai/realtime-voice-provider.ts b/extensions/openai/realtime-voice-provider.ts index 5d2582ea4fc5..28fc62f15e3a 100644 --- a/extensions/openai/realtime-voice-provider.ts +++ b/extensions/openai/realtime-voice-provider.ts @@ -362,7 +362,7 @@ async function resolveOpenAIRealtimeDefaultAuth(params: { if (prefersCodexOAuthForRealtimeModel(params.model)) { const codexToken = await resolveProviderAuthProfileApiKey({ - provider: "openai-codex", + provider: "openai", cfg: params.cfg, includeExternalCliAuth: true, }); @@ -401,7 +401,7 @@ function hasOpenAIRealtimeBrowserAuthInput(params: { if (prefersCodexOAuthForRealtimeModel(params.model)) { return ( isProviderAuthProfileConfigured({ - provider: "openai-codex", + provider: "openai", cfg: params.cfg, includeExternalCliAuth: true, }) || hasOpenAIRealtimeApiKeyInput(undefined) @@ -744,7 +744,7 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge { if ( !isProviderAuthProfileConfigured({ - provider: "openai-codex", + provider: "openai", cfg: cfg.cfg, includeExternalCliAuth: true, }) diff --git a/extensions/openai/register.runtime.ts b/extensions/openai/register.runtime.ts index 97c00010d022..483dedc6e1b5 100644 --- a/extensions/openai/register.runtime.ts +++ b/extensions/openai/register.runtime.ts @@ -1,9 +1,5 @@ export { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js"; -export { - openaiCodexMediaUnderstandingProvider, - openaiMediaUnderstandingProvider, -} from "./media-understanding-provider.js"; -export { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js"; +export { openaiMediaUnderstandingProvider } from "./media-understanding-provider.js"; export { buildOpenAIProvider } from "./openai-provider.js"; export { OPENAI_FRIENDLY_PROMPT_OVERLAY, diff --git a/extensions/openai/setup-api.test.ts b/extensions/openai/setup-api.test.ts index 1e295938fcf8..7e753838e993 100644 --- a/extensions/openai/setup-api.test.ts +++ b/extensions/openai/setup-api.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildOpenAICodexSetupProvider, buildOpenAISetupProvider } from "./setup-api.js"; +import { buildOpenAISetupProvider } from "./setup-api.js"; function authMethodIds(provider: ReturnType) { return provider.auth.map((method) => method.id); @@ -12,6 +12,7 @@ describe("OpenAI setup auth provider", () => { const apiKey = provider.auth.find((method) => method.id === "api-key"); expect(provider.id).toBe("openai"); + expect(provider.aliases).toEqual(["openai-codex"]); expect(authMethodIds(provider)).toEqual(["oauth", "device-code", "api-key"]); expect(oauth?.label).toBe("ChatGPT Login"); expect(oauth?.wizard?.choiceId).toBe("openai"); @@ -19,11 +20,4 @@ describe("OpenAI setup auth provider", () => { expect(apiKey?.label).toBe("OpenAI API Key"); expect(apiKey?.wizard?.choiceId).toBe("openai-api-key"); }); - - it("keeps the legacy openai-codex setup provider available", () => { - const provider = buildOpenAICodexSetupProvider(); - - expect(provider.id).toBe("openai-codex"); - expect(authMethodIds(provider)).toEqual(["oauth", "device-code", "api-key"]); - }); }); diff --git a/extensions/openai/setup-api.ts b/extensions/openai/setup-api.ts index 342c6b2428ad..5003640d2ab1 100644 --- a/extensions/openai/setup-api.ts +++ b/extensions/openai/setup-api.ts @@ -9,13 +9,6 @@ import { OPENAI_CHATGPT_DEVICE_PAIRING_LABEL, OPENAI_CHATGPT_LOGIN_HINT, OPENAI_CHATGPT_LOGIN_LABEL, - OPENAI_CODEX_API_KEY_BACKUP_HINT, - OPENAI_CODEX_API_KEY_BACKUP_LABEL, - OPENAI_CODEX_DEVICE_PAIRING_HINT, - OPENAI_CODEX_DEVICE_PAIRING_LABEL, - OPENAI_CODEX_LOGIN_HINT, - OPENAI_CODEX_LOGIN_LABEL, - OPENAI_CODEX_WIZARD_GROUP, } from "./auth-choice-copy.js"; async function runOpenAIProviderAuthMethod( @@ -30,18 +23,6 @@ async function runOpenAIProviderAuthMethod( return method.run(ctx); } -async function runOpenAICodexProviderAuthMethod( - methodId: string, - ctx: ProviderAuthContext, -): Promise { - const { buildOpenAICodexProviderPlugin } = await import("./openai-codex-provider.js"); - const method = buildOpenAICodexProviderPlugin().auth.find((entry) => entry.id === methodId); - if (!method) { - return { profiles: [] }; - } - return method.run(ctx); -} - export function buildOpenAISetupProvider(): ProviderPlugin { const oauthMethod = { id: "oauth", @@ -56,7 +37,7 @@ export function buildOpenAISetupProvider(): ProviderPlugin { assistantVisibility: "manual-only", ...OPENAI_ACCOUNT_WIZARD_GROUP, }, - run: async (ctx) => runOpenAICodexProviderAuthMethod("oauth", ctx), + run: async (ctx) => runOpenAIProviderAuthMethod("oauth", ctx), } satisfies ProviderAuthMethod; const deviceCodeMethod = { @@ -72,7 +53,7 @@ export function buildOpenAISetupProvider(): ProviderPlugin { assistantVisibility: "manual-only", ...OPENAI_ACCOUNT_WIZARD_GROUP, }, - run: async (ctx) => runOpenAICodexProviderAuthMethod("device-code", ctx), + run: async (ctx) => runOpenAIProviderAuthMethod("device-code", ctx), } satisfies ProviderAuthMethod; const apiKeyMethod = { @@ -92,6 +73,7 @@ export function buildOpenAISetupProvider(): ProviderPlugin { return { id: "openai", + aliases: ["openai-codex"], label: "OpenAI", docsPath: "/providers/models", envVars: ["OPENAI_API_KEY"], @@ -99,68 +81,11 @@ export function buildOpenAISetupProvider(): ProviderPlugin { }; } -export function buildOpenAICodexSetupProvider(): ProviderPlugin { - const oauthMethod = { - id: "oauth", - label: OPENAI_CODEX_LOGIN_LABEL, - hint: OPENAI_CODEX_LOGIN_HINT, - kind: "oauth", - wizard: { - choiceId: "openai-codex", - choiceLabel: OPENAI_CODEX_LOGIN_LABEL, - choiceHint: OPENAI_CODEX_LOGIN_HINT, - assistantPriority: -30, - onboardingFeatured: true, - ...OPENAI_CODEX_WIZARD_GROUP, - }, - run: async (ctx) => runOpenAICodexProviderAuthMethod("oauth", ctx), - } satisfies ProviderAuthMethod; - - const deviceCodeMethod = { - id: "device-code", - label: OPENAI_CODEX_DEVICE_PAIRING_LABEL, - hint: OPENAI_CODEX_DEVICE_PAIRING_HINT, - kind: "device_code", - wizard: { - choiceId: "openai-codex-device-code", - choiceLabel: OPENAI_CODEX_DEVICE_PAIRING_LABEL, - choiceHint: OPENAI_CODEX_DEVICE_PAIRING_HINT, - assistantPriority: -10, - ...OPENAI_CODEX_WIZARD_GROUP, - }, - run: async (ctx) => runOpenAICodexProviderAuthMethod("device-code", ctx), - } satisfies ProviderAuthMethod; - - const apiKeyBackupMethod = { - id: "api-key", - label: OPENAI_CODEX_API_KEY_BACKUP_LABEL, - hint: OPENAI_CODEX_API_KEY_BACKUP_HINT, - kind: "api_key", - wizard: { - choiceId: "openai-codex-api-key", - choiceLabel: OPENAI_CODEX_API_KEY_BACKUP_LABEL, - choiceHint: OPENAI_CODEX_API_KEY_BACKUP_HINT, - assistantPriority: 5, - assistantVisibility: "manual-only", - ...OPENAI_CODEX_WIZARD_GROUP, - }, - run: async (ctx) => runOpenAICodexProviderAuthMethod("api-key", ctx), - } satisfies ProviderAuthMethod; - - return { - id: "openai-codex", - label: "OpenAI Codex", - docsPath: "/providers/models", - auth: [oauthMethod, deviceCodeMethod, apiKeyBackupMethod], - }; -} - export default definePluginEntry({ id: "openai", name: "OpenAI Setup", description: "Lightweight OpenAI setup hooks", register(api) { api.registerProvider(buildOpenAISetupProvider()); - api.registerProvider(buildOpenAICodexSetupProvider()); }, }); diff --git a/extensions/openai/test-api.ts b/extensions/openai/test-api.ts index 0f20b19cb4c7..434036a149c5 100644 --- a/extensions/openai/test-api.ts +++ b/extensions/openai/test-api.ts @@ -1,8 +1,5 @@ export { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js"; -export { - openaiCodexMediaUnderstandingProvider, - openaiMediaUnderstandingProvider, -} from "./media-understanding-provider.js"; +export { openaiMediaUnderstandingProvider } from "./media-understanding-provider.js"; export { buildOpenAIRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js"; export { buildOpenAIRealtimeVoiceProvider } from "./realtime-voice-provider.js"; export { buildOpenAISpeechProvider } from "./speech-provider.js"; diff --git a/extensions/openai/transport-policy.ts b/extensions/openai/transport-policy.ts index 07ff4d1e0bec..a5558e6f76a2 100644 --- a/extensions/openai/transport-policy.ts +++ b/extensions/openai/transport-policy.ts @@ -10,7 +10,6 @@ import { isOpenAIApiBaseUrl, isOpenAICodexBaseUrl } from "./base-url.js"; const DEFAULT_OPENAI_WS_DEGRADE_COOLDOWN_MS = 60_000; const AZURE_PROVIDER_IDS = new Set(["azure-openai", "azure-openai-responses"]); -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; function isAzureOpenAIBaseUrl(baseUrl?: string): boolean { const trimmed = baseUrl?.trim(); @@ -35,14 +34,11 @@ function usesKnownNativeOpenAIRoute(provider: string, baseUrl?: string): boolean return false; } if (normalizedProvider === "openai") { - return !baseUrl || isOpenAIApiBaseUrl(baseUrl); + return !baseUrl || isOpenAIApiBaseUrl(baseUrl) || isOpenAICodexBaseUrl(baseUrl); } if (AZURE_PROVIDER_IDS.has(normalizedProvider)) { return !baseUrl || isAzureOpenAIBaseUrl(baseUrl); } - if (normalizedProvider === OPENAI_CODEX_PROVIDER_ID) { - return !baseUrl || isOpenAIApiBaseUrl(baseUrl) || isOpenAICodexBaseUrl(baseUrl); - } return false; } diff --git a/extensions/openai/video-generation-provider.test.ts b/extensions/openai/video-generation-provider.test.ts index c2de84c3e61f..6c92ae91e58b 100644 --- a/extensions/openai/video-generation-provider.test.ts +++ b/extensions/openai/video-generation-provider.test.ts @@ -1,3 +1,6 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { getProviderHttpMocks, installProviderHttpMockCleanup, @@ -6,6 +9,7 @@ import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/p import { beforeAll, describe, expect, it, vi } from "vitest"; const { + resolveApiKeyForProviderMock, postJsonRequestMock, postMultipartRequestMock, fetchWithTimeoutMock, @@ -100,12 +104,6 @@ function streamedVideoResponse(bytes: string): Response { } describe("openai video generation provider", () => { - it("declares the openai-codex alias for default-model ordering", () => { - const provider = buildOpenAIVideoGenerationProvider(); - - expect(provider.aliases).toContain("openai-codex"); - }); - it("declares explicit mode capabilities", () => { expectExplicitVideoGenerationCapabilities(buildOpenAIVideoGenerationProvider()); }); @@ -120,6 +118,63 @@ describe("openai video generation provider", () => { }); }); + it("does not advertise video generation for OAuth-only OpenAI profiles", () => { + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-openai-video-auth-")); + const previousOpenAIKey = process.env.OPENAI_API_KEY; + delete process.env.OPENAI_API_KEY; + try { + fs.writeFileSync( + path.join(agentDir, "auth-profiles.json"), + JSON.stringify({ + version: 1, + profiles: { + "openai:chatgpt": { + type: "oauth", + provider: "openai", + access: "chatgpt-oauth-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + }, + }, + }), + ); + + expect(buildOpenAIVideoGenerationProvider().isConfigured?.({ agentDir })).toBe(false); + } finally { + if (previousOpenAIKey === undefined) { + delete process.env.OPENAI_API_KEY; + } else { + process.env.OPENAI_API_KEY = previousOpenAIKey; + } + fs.rmSync(agentDir, { recursive: true, force: true }); + } + }); + + it("requires an OpenAI API key credential for direct video generation", async () => { + resolveApiKeyForProviderMock.mockResolvedValueOnce({ + apiKey: "chatgpt-oauth-token", + mode: "oauth", + } as never); + + const provider = buildOpenAIVideoGenerationProvider(); + await expect( + provider.generateVideo({ + provider: "openai", + model: "sora-2", + prompt: "A paper airplane gliding through golden hour light", + cfg: {}, + }), + ).rejects.toThrow("OpenAI API key missing"); + + expect(resolveApiKeyForProviderMock).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "openai", + modelApi: "openai-responses", + }), + ); + expect(postJsonRequestMock).not.toHaveBeenCalled(); + }); + it("uses JSON for text-only Sora requests", async () => { postJsonRequestMock.mockResolvedValue({ response: { diff --git a/extensions/openai/video-generation-provider.ts b/extensions/openai/video-generation-provider.ts index 5da8ce0da84d..cc27821a13c4 100644 --- a/extensions/openai/video-generation-provider.ts +++ b/extensions/openai/video-generation-provider.ts @@ -281,7 +281,6 @@ async function downloadOpenAIVideo( export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider { return { id: "openai", - aliases: ["openai-codex"], label: "OpenAI", defaultModel: DEFAULT_OPENAI_VIDEO_MODEL, models: [DEFAULT_OPENAI_VIDEO_MODEL, "sora-2-pro"], @@ -289,6 +288,7 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider { isProviderApiKeyConfigured({ provider: "openai", agentDir, + profileTypes: ["api_key"], }), capabilities: { generate: { @@ -319,8 +319,9 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider { cfg: req.cfg, agentDir: req.agentDir, store: req.authStore, + modelApi: "openai-responses", }); - if (!auth.apiKey) { + if (!auth.apiKey || (auth.mode !== undefined && auth.mode !== "api-key")) { throw new Error("OpenAI API key missing"); } diff --git a/src/agents/auth-profile-runtime-contract.test.ts b/src/agents/auth-profile-runtime-contract.test.ts index 15c647edd0b6..aa1fc1525f42 100644 --- a/src/agents/auth-profile-runtime-contract.test.ts +++ b/src/agents/auth-profile-runtime-contract.test.ts @@ -241,12 +241,9 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () [AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider], [ AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider, - AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider, - ], - [ - AUTH_PROFILE_RUNTIME_CONTRACT.codexCliProvider, - AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider, + AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, ], + [AUTH_PROFILE_RUNTIME_CONTRACT.codexCliProvider, AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider], [ AUTH_PROFILE_RUNTIME_CONTRACT.codexHarnessProvider, AUTH_PROFILE_RUNTIME_CONTRACT.codexHarnessProvider, @@ -263,7 +260,7 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () }, ); - it("forwards an OpenAI Codex auth profile when the selected provider is codex-cli", async () => { + it("forwards a legacy OpenAI Codex auth profile when the selected provider is codex-cli", async () => { const { aliasLookupParams } = await runAuthContractAttempt({ tmpDir, storePath, @@ -282,7 +279,7 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () ); }); - it("forwards an OpenAI Codex auth profile when the auth provider is the legacy codex-cli alias", async () => { + it("forwards a legacy OpenAI Codex auth profile when the auth provider is the legacy codex-cli alias", async () => { const { aliasLookupParams } = await runAuthContractAttempt({ tmpDir, storePath, @@ -301,8 +298,8 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () ); }); - it("does not leak an OpenAI API-key auth profile into the Codex CLI alias", async () => { - await runAuthContractAttempt({ + it("forwards OpenAI auth profiles into the Codex CLI alias", async () => { + const { aliasLookupParams } = await runAuthContractAttempt({ tmpDir, storePath, providerOverride: AUTH_PROFILE_RUNTIME_CONTRACT.codexCliProvider, @@ -310,7 +307,14 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () authProfileOverride: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProfileId, }); - expect(capturedCliRunParams().authProfileId).toBeUndefined(); + expect(capturedCliRunParams().authProfileId).toBe( + expectedForwardedAuthProfile({ + provider: AUTH_PROFILE_RUNTIME_CONTRACT.codexCliProvider, + authProfileProvider: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, + aliasLookupParams, + sessionAuthProfileId: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProfileId, + }), + ); }); it("does not leak an OpenAI Codex auth profile into an unrelated CLI provider", async () => { @@ -348,7 +352,7 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () expect(capturedCliRunParams().authProfileId).toBeUndefined(); }); - it("forwards an OpenAI Codex auth profile through the embedded OpenClaw path", async () => { + it("forwards a legacy OpenAI Codex auth profile through the embedded OpenClaw path", async () => { await runAuthContractAttempt({ tmpDir, storePath, @@ -362,7 +366,7 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () ); }); - it("accepts the legacy codex-cli auth-provider alias on the embedded OpenAI Codex path", async () => { + it("accepts the legacy codex-cli auth-provider alias on the embedded OpenAI path", async () => { const { aliasLookupParams } = await runAuthContractAttempt({ tmpDir, storePath, @@ -396,7 +400,7 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () expect(params.authProfileId).toBe(AUTH_PROFILE_RUNTIME_CONTRACT.openAiProfileId); }); - it("forwards an OpenAI Codex auth profile through the default OpenAI Codex harness path", async () => { + it("forwards a legacy OpenAI Codex auth profile through the default OpenAI harness path", async () => { await runAuthContractAttempt({ tmpDir, storePath, @@ -410,7 +414,7 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () ); }); - it("routes explicit OpenAI OpenClaw runs with Codex OAuth through OpenAI Codex transport", async () => { + it("routes explicit OpenAI OpenClaw runs with legacy Codex OAuth through OpenAI transport", async () => { await runAuthContractAttempt({ tmpDir, storePath, @@ -421,7 +425,7 @@ describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () }); const params = capturedEmbeddedRunParams(); - expect(params.provider).toBe(AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider); + expect(params.provider).toBe(AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider); expect(params.authProfileId).toBe(AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId); }); diff --git a/src/agents/auth-profiles.external-cli-sync.test.ts b/src/agents/auth-profiles.external-cli-sync.test.ts index b17cb509da56..e485bfbec6e9 100644 --- a/src/agents/auth-profiles.external-cli-sync.test.ts +++ b/src/agents/auth-profiles.external-cli-sync.test.ts @@ -320,7 +320,7 @@ describe("external cli oauth resolution", () => { expectCredentialFields( expectSingleProfileCredential(profiles, OPENAI_CODEX_DEFAULT_PROFILE_ID), { - provider: "openai-codex", + provider: "openai", access: "codex-cli-access", refresh: "codex-cli-refresh", accountId: "acct-codex", @@ -510,7 +510,7 @@ describe("external cli oauth resolution", () => { expectSingleProfileCredential(profiles, OPENAI_CODEX_DEFAULT_PROFILE_ID), { type: "oauth", - provider: "openai-codex", + provider: "openai", }, ); expectReaderPolicyCall(mocks.readCodexCliCredentialsCached); diff --git a/src/agents/auth-profiles/constants.ts b/src/agents/auth-profiles/constants.ts index bf32e0397a4c..34940d27a9e7 100644 --- a/src/agents/auth-profiles/constants.ts +++ b/src/agents/auth-profiles/constants.ts @@ -9,9 +9,9 @@ export const AUTH_STORE_VERSION = 1; /** @deprecated Anthropic provider-owned CLI profile id; do not use from third-party plugins. */ export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli"; -/** @deprecated OpenAI Codex provider-owned CLI profile id; do not use from third-party plugins. */ -export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli"; -export const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default"; +/** @deprecated OpenAI provider-owned CLI profile id; do not use from third-party plugins. */ +export const CODEX_CLI_PROFILE_ID = "openai:codex-cli"; +export const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai:default"; /** @deprecated MiniMax provider-owned CLI profile id; do not use from third-party plugins. */ export const MINIMAX_CLI_PROFILE_ID = "minimax-portal:minimax-cli"; diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index da5db3441b04..af60acad43f9 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -42,6 +42,7 @@ export type ExternalCliAuthProfileOptions = { type ExternalCliSyncProvider = { profileId: string; + profileAliases?: readonly string[]; provider: string; aliases?: readonly string[]; readCredentials: ( @@ -98,8 +99,9 @@ export function isSafeToUseExternalCliCredential( const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [ { profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, - provider: "openai-codex", - aliases: ["codex", "codex-cli", "codex-app-server"], + profileAliases: ["openai-codex:default"], + provider: "openai", + aliases: ["openai-codex", "codex", "codex-cli", "codex-app-server"], readCredentials: (options) => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS, @@ -133,18 +135,58 @@ function resolveExternalCliSyncProvider(params: { profileId: string; credential?: OAuthCredential; }): ExternalCliSyncProvider | null { - const provider = EXTERNAL_CLI_SYNC_PROVIDERS.find( - (entry) => entry.profileId === params.profileId, + const provider = EXTERNAL_CLI_SYNC_PROVIDERS.find((entry) => + externalCliProfileIdMatches(entry, params.profileId), ); if (!provider) { return null; } - if (params.credential && provider.provider !== params.credential.provider) { + if ( + params.credential && + !listExternalCliProviderIds(provider).includes(params.credential.provider) + ) { return null; } return provider; } +function listExternalCliProfileIds(providerConfig: ExternalCliSyncProvider): string[] { + return [providerConfig.profileId, ...(providerConfig.profileAliases ?? [])]; +} + +function listExternalCliProviderIds(providerConfig: ExternalCliSyncProvider): string[] { + return [providerConfig.provider, ...(providerConfig.aliases ?? [])]; +} + +function normalizeExternalCliCredentialProvider( + credential: OAuthCredential | null, + provider: string, +): OAuthCredential | null { + return credential ? { ...credential, provider } : null; +} + +function getAuthProfileProviderPrefix(profileId: string): string { + return profileId.split(":", 1)[0]?.trim() ?? ""; +} + +function externalCliProfileIdMatches( + providerConfig: ExternalCliSyncProvider, + profileId: string, + options?: { allowLegacyNamespace?: boolean }, +): boolean { + if (listExternalCliProfileIds(providerConfig).includes(profileId)) { + return true; + } + if ( + !options?.allowLegacyNamespace || + providerConfig.profileId !== OPENAI_CODEX_DEFAULT_PROFILE_ID + ) { + return false; + } + const normalizedPrefix = normalizeProviderId(getAuthProfileProviderPrefix(profileId)); + return normalizedPrefix === "openai-codex"; +} + function hasInlineOAuthTokenMaterial(credential: OAuthCredential): boolean { return [credential.access, credential.refresh, credential.idToken].some( (value) => typeof value === "string" && value.trim().length > 0, @@ -168,7 +210,10 @@ export function readExternalCliBootstrapCredential(params: { ) { return null; } - return provider.readCredentials({ allowKeychainPrompt: params.allowKeychainPrompt }); + return normalizeExternalCliCredentialProvider( + provider.readCredentials({ allowKeychainPrompt: params.allowKeychainPrompt }), + params.credential.provider, + ); } export const readManagedExternalCliCredential = readExternalCliBootstrapCredential; @@ -180,11 +225,16 @@ export function readExternalCliFallbackCredential(params: { }): OAuthCredential | null { const provider = resolveExternalCliSyncProvider(params) ?? - EXTERNAL_CLI_SYNC_PROVIDERS.find((entry) => entry.provider === params.credential.provider); + EXTERNAL_CLI_SYNC_PROVIDERS.find((entry) => + listExternalCliProviderIds(entry).includes(params.credential.provider), + ); if (!provider) { return null; } - return provider.readCredentials({ allowKeychainPrompt: params.allowKeychainPrompt }); + return normalizeExternalCliCredentialProvider( + provider.readCredentials({ allowKeychainPrompt: params.allowKeychainPrompt }), + params.credential.provider, + ); } function normalizeProviderScope(values: Iterable | undefined): Set | undefined { @@ -206,20 +256,6 @@ function normalizeProviderScope(values: Iterable | undefined): Set | undefined): Set | undefined { - if (values === undefined) { - return undefined; - } - const out = new Set(); - for (const value of values) { - const raw = value.trim().toLowerCase(); - if (raw) { - out.add(raw); - } - } - return out; -} - function isExternalCliProviderInScope(params: { providerConfig: ExternalCliSyncProvider; store: AuthProfileStore; @@ -227,25 +263,61 @@ function isExternalCliProviderInScope(params: { }): boolean { const { providerConfig, options, store } = params; const providerScope = normalizeProviderScope(options?.providerIds); - const profileScope = normalizeProfileScope(options?.profileIds); - if (providerScope === undefined && profileScope === undefined) { - const existing = store.profiles[providerConfig.profileId]; - return existing?.type === "oauth" && existing.provider === providerConfig.provider; + if (providerScope === undefined && options?.profileIds === undefined) { + return Object.entries(store.profiles).some(([profileId, existing]) => { + return ( + externalCliProfileIdMatches(providerConfig, profileId) && + existing?.type === "oauth" && + listExternalCliProviderIds(providerConfig).includes(existing.provider) + ); + }); } - if (profileScope?.has(providerConfig.profileId.toLowerCase())) { + if ( + Array.from(options?.profileIds ?? []).some((profileId) => + externalCliProfileIdMatches(providerConfig, profileId.trim(), { + allowLegacyNamespace: true, + }), + ) + ) { return true; } if (!providerScope || providerScope.size === 0) { return false; } - const aliases = [providerConfig.provider, ...(providerConfig.aliases ?? [])]; - return aliases.some((alias) => { + return listExternalCliProviderIds(providerConfig).some((alias) => { const raw = alias.trim().toLowerCase(); const normalized = normalizeProviderId(alias); return providerScope.has(raw) || (normalized ? providerScope.has(normalized) : false); }); } +function listScopedExternalCliProfileIds(params: { + providerConfig: ExternalCliSyncProvider; + store: AuthProfileStore; + options?: ExternalCliAuthProfileOptions; +}): string[] { + const { options, providerConfig, store } = params; + const requestedProfileIds = Array.from(options?.profileIds ?? []) + .map((value) => value.trim()) + .filter((value) => value.length > 0); + if (requestedProfileIds.length > 0) { + return requestedProfileIds.filter((profileId) => + externalCliProfileIdMatches(providerConfig, profileId, { + allowLegacyNamespace: true, + }), + ); + } + + const existingProfileIds = Object.keys(store.profiles).filter((profileId) => + externalCliProfileIdMatches(providerConfig, profileId), + ); + if (existingProfileIds.length > 0) { + return existingProfileIds; + } + + return options?.providerIds ? [providerConfig.profileId] : []; +} + export function resolveExternalCliAuthProfiles( store: AuthProfileStore, options?: ExternalCliAuthProfileOptions, @@ -256,90 +328,101 @@ export function resolveExternalCliAuthProfiles( if (!isExternalCliProviderInScope({ providerConfig, store, options })) { continue; } - const existing = store.profiles[providerConfig.profileId]; - const existingOAuth = - existing?.type === "oauth" && existing.provider === providerConfig.provider - ? existing - : undefined; - if (existing && !existingOAuth) { - log.debug("kept explicit local auth over external cli bootstrap", { - profileId: providerConfig.profileId, - provider: providerConfig.provider, - localType: existing.type, - localProvider: existing.provider, - }); - continue; - } - if ( - providerConfig.bootstrapOnly && - existingOAuth && - hasInlineOAuthTokenMaterial(existingOAuth) - ) { - log.debug("kept local oauth over external cli bootstrap-only provider", { - profileId: providerConfig.profileId, - provider: providerConfig.provider, - }); - continue; - } - if ( - existingOAuth && - !providerConfig.bootstrapOnly && - hasUsableOAuthCredential(existingOAuth, now) - ) { - continue; - } - const creds = providerConfig.readCredentials({ - allowKeychainPrompt: options?.allowKeychainPrompt, + const scopedProfileIds = listScopedExternalCliProfileIds({ + providerConfig, + store, + options, }); - if (!creds) { - continue; - } - if (existingOAuth && !isSafeToUseExternalCliCredential(existingOAuth, creds)) { - log.warn("refused external cli oauth bootstrap: identity mismatch", { - profileId: providerConfig.profileId, - provider: providerConfig.provider, - }); - continue; - } - if ( - existingOAuth && - !isSafeToAdoptBootstrapOAuthIdentity(existingOAuth, creds) && - !areOAuthCredentialsEquivalent(existingOAuth, creds) - ) { - log.warn("refused external cli oauth bootstrap: identity mismatch or missing binding", { - profileId: providerConfig.profileId, - provider: providerConfig.provider, - }); - continue; - } - if ( - !shouldBootstrapFromExternalCliCredential({ - existing: existingOAuth, - imported: creds, - now, - }) - ) { - if (existingOAuth) { - log.debug("kept usable local oauth over external cli bootstrap", { - profileId: providerConfig.profileId, + for (const profileId of scopedProfileIds) { + const existing = store.profiles[profileId]; + const existingOAuth = + existing?.type === "oauth" && + listExternalCliProviderIds(providerConfig).includes(existing.provider) + ? existing + : undefined; + if (existing && !existingOAuth) { + log.debug("kept explicit local auth over external cli bootstrap", { + profileId, provider: providerConfig.provider, - localExpires: existingOAuth.expires, - externalExpires: creds.expires, + localType: existing.type, + localProvider: existing.provider, }); + continue; } - continue; + if ( + providerConfig.bootstrapOnly && + existingOAuth && + hasInlineOAuthTokenMaterial(existingOAuth) + ) { + log.debug("kept local oauth over external cli bootstrap-only provider", { + profileId, + provider: providerConfig.provider, + }); + continue; + } + if ( + existingOAuth && + !providerConfig.bootstrapOnly && + hasUsableOAuthCredential(existingOAuth, now) + ) { + continue; + } + const creds = normalizeExternalCliCredentialProvider( + providerConfig.readCredentials({ + allowKeychainPrompt: options?.allowKeychainPrompt, + }), + existingOAuth?.provider ?? providerConfig.provider, + ); + if (!creds) { + continue; + } + if (existingOAuth && !isSafeToUseExternalCliCredential(existingOAuth, creds)) { + log.warn("refused external cli oauth bootstrap: identity mismatch", { + profileId, + provider: providerConfig.provider, + }); + continue; + } + if ( + existingOAuth && + !isSafeToAdoptBootstrapOAuthIdentity(existingOAuth, creds) && + !areOAuthCredentialsEquivalent(existingOAuth, creds) + ) { + log.warn("refused external cli oauth bootstrap: identity mismatch or missing binding", { + profileId, + provider: providerConfig.provider, + }); + continue; + } + if ( + !shouldBootstrapFromExternalCliCredential({ + existing: existingOAuth, + imported: creds, + now, + }) + ) { + if (existingOAuth) { + log.debug("kept usable local oauth over external cli bootstrap", { + profileId, + provider: providerConfig.provider, + localExpires: existingOAuth.expires, + externalExpires: creds.expires, + }); + } + continue; + } + log.debug("used external cli oauth bootstrap because local oauth was missing or unusable", { + profileId, + provider: providerConfig.provider, + localExpires: existingOAuth?.expires, + externalExpires: creds.expires, + }); + profiles.push({ + profileId, + credential: creds, + persistence: providerConfig.bootstrapOnly ? "runtime-only" : "persisted", + }); } - log.debug("used external cli oauth bootstrap because local oauth was missing or unusable", { - profileId: providerConfig.profileId, - provider: providerConfig.provider, - localExpires: existingOAuth?.expires, - externalExpires: creds.expires, - }); - profiles.push({ - profileId: providerConfig.profileId, - credential: creds, - persistence: providerConfig.bootstrapOnly ? "runtime-only" : "persisted", - }); } return profiles; } diff --git a/src/agents/auth-profiles/external-oauth.test.ts b/src/agents/auth-profiles/external-oauth.test.ts index 7170e1764911..15f0de4127de 100644 --- a/src/agents/auth-profiles/external-oauth.test.ts +++ b/src/agents/auth-profiles/external-oauth.test.ts @@ -104,6 +104,31 @@ describe("auth external oauth helpers", () => { expect(readCodexCliCredentialsCachedMock).toHaveBeenCalledTimes(1); }); + it("does not bootstrap arbitrary named OpenAI OAuth profiles from the Codex CLI account", () => { + readCodexCliCredentialsCachedMock.mockReturnValueOnce( + createCredential({ + provider: "openai", + access: "codex-cli-access", + refresh: "codex-cli-refresh", + expires: createUsableOAuthExpiry(), + }), + ); + + const store = createStore({ + "openai:work": createCredential({ + provider: "openai", + access: undefined, + refresh: undefined, + expires: 0, + }), + }); + + const overlaid = overlayExternalOAuthProfiles(store); + + expect(readCodexCliCredentialsCachedMock).not.toHaveBeenCalled(); + expect(overlaid.profiles["openai:work"]).toEqual(store.profiles["openai:work"]); + }); + it("omits exact runtime-only overlays from persisted store writes", () => { const credential = createCredential(); resolveExternalAuthProfilesWithPluginsMock.mockReturnValueOnce([ diff --git a/src/agents/auth-profiles/oauth-refresh-failure.ts b/src/agents/auth-profiles/oauth-refresh-failure.ts index a010919ab708..24f929796589 100644 --- a/src/agents/auth-profiles/oauth-refresh-failure.ts +++ b/src/agents/auth-profiles/oauth-refresh-failure.ts @@ -11,6 +11,8 @@ export type OAuthRefreshFailureReason = const OAUTH_REFRESH_FAILURE_PROVIDER_RE = /OAuth token refresh failed for ([^:]+):/i; const SAFE_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]*$/; +const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const OPENAI_PROVIDER_ID = "openai"; function isOAuthRefreshFailureMessage(message: string): boolean { const lower = message.toLowerCase(); @@ -32,6 +34,10 @@ function sanitizeOAuthRefreshFailureProvider(provider: string | null | undefined return normalized && SAFE_PROVIDER_ID_RE.test(normalized) ? normalized : null; } +function canonicalizeOAuthRefreshFailureProvider(provider: string | null): string | null { + return provider === LEGACY_OPENAI_CODEX_PROVIDER_ID ? OPENAI_PROVIDER_ID : provider; +} + export function classifyOAuthRefreshFailureReason( message: string, ): OAuthRefreshFailureReason | null { @@ -62,14 +68,18 @@ export function classifyOAuthRefreshFailure(message: string): { return null; } return { - provider: sanitizeOAuthRefreshFailureProvider(extractOAuthRefreshFailureProvider(message)), + provider: canonicalizeOAuthRefreshFailureProvider( + sanitizeOAuthRefreshFailureProvider(extractOAuthRefreshFailureProvider(message)), + ), reason: classifyOAuthRefreshFailureReason(message), }; } export function buildOAuthRefreshFailureLoginCommand(provider: string | null | undefined): string { - const safeProvider = sanitizeOAuthRefreshFailureProvider(provider); - return safeProvider - ? formatCliCommand(`openclaw models auth login --provider ${safeProvider}`) + const canonicalProvider = canonicalizeOAuthRefreshFailureProvider( + sanitizeOAuthRefreshFailureProvider(provider), + ); + return canonicalProvider + ? formatCliCommand(`openclaw models auth login --provider ${canonicalProvider}`) : formatCliCommand("openclaw models auth login"); } diff --git a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts index 1bedf9deb9e4..c02d95e6e6af 100644 --- a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts +++ b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts @@ -841,7 +841,7 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => { profileId: resolved.profileId ?? "", agentDir, }); - expect(ensureAuthProfileStore(agentDir).lastGood?.["openai-codex"]).toBe(healthyProfileId); + expect(ensureAuthProfileStore(agentDir).lastGood?.openai).toBe(healthyProfileId); }); it("retries Codex refresh once after refresh_token_reused updates only the stored refresh token", async () => { diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index e5eedd098c24..b0a7058211d2 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -223,7 +223,7 @@ const oauthManager = createOAuthManager({ credential, }), readFallbackCredential: ({ profileId, credential }) => - credential.provider === "openai-codex" + credential.provider === "openai" || credential.provider === "openai-codex" ? readExternalCliFallbackCredential({ profileId, credential, diff --git a/src/agents/auth-profiles/profiles.ts b/src/agents/auth-profiles/profiles.ts index 5bf4084ff641..358937030d2a 100644 --- a/src/agents/auth-profiles/profiles.ts +++ b/src/agents/auth-profiles/profiles.ts @@ -11,6 +11,19 @@ import { import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js"; export { dedupeProfileIds, listProfilesForProvider } from "./profile-list.js"; +function findProviderAuthStateKey( + entries: Record | undefined, + providerKey: string, +): string | undefined { + if (!entries) { + return undefined; + } + const normalizedProviderKey = resolveProviderIdForAuth(providerKey); + return Object.keys(entries).find( + (key) => resolveProviderIdForAuth(key) === normalizedProviderKey, + ); +} + function resetSuccessfulUsageStats( existing: ProfileUsageStats | undefined, lastUsed: number, @@ -85,7 +98,9 @@ export async function promoteAuthProfileInOrder(params: { return false; } const orderKey = - findNormalizedProviderKey(store.order, providerKey) ?? normalizeProviderId(providerKey); + findProviderAuthStateKey(store.order, providerKey) ?? + findNormalizedProviderKey(store.order, providerKey) ?? + normalizeProviderId(providerKey); const existing = store.order?.[orderKey]; if (!existing || existing.length === 0) { return false; @@ -214,10 +229,11 @@ export async function clearLastGoodProfileWithLock(params: { return await updateAuthProfileStoreWithLock({ agentDir: params.agentDir, updater: (store) => { - if (store.lastGood?.[providerKey] !== params.profileId) { + const lastGoodKey = findProviderAuthStateKey(store.lastGood, providerKey); + if (!lastGoodKey || store.lastGood?.[lastGoodKey] !== params.profileId) { return false; } - delete store.lastGood[providerKey]; + delete store.lastGood[lastGoodKey]; if (Object.keys(store.lastGood).length === 0) { store.lastGood = undefined; } diff --git a/src/agents/auth-profiles/usage.ts b/src/agents/auth-profiles/usage.ts index b2f996846175..768b58be543c 100644 --- a/src/agents/auth-profiles/usage.ts +++ b/src/agents/auth-profiles/usage.ts @@ -109,8 +109,9 @@ function shouldProbeWhamForFailure( provider: string | undefined, reason: AuthProfileFailureReason, ): boolean { + const normalizedProvider = normalizeProviderId(provider ?? ""); return ( - normalizeProviderId(provider ?? "") === "openai-codex" && + (normalizedProvider === "openai" || normalizedProvider === "openai-codex") && (reason === "rate_limit" || reason === "empty_response" || reason === "no_error_details" || @@ -209,7 +210,7 @@ async function probeWhamForCooldown( } const headers = resolveProviderRequestHeaders({ - provider: "openai-codex", + provider: "openai", baseUrl: WHAM_USAGE_URL, capability: "other", transport: "http", diff --git a/src/agents/codex-native-web-search-core.ts b/src/agents/codex-native-web-search-core.ts index c504b3ff2fab..12d107842446 100644 --- a/src/agents/codex-native-web-search-core.ts +++ b/src/agents/codex-native-web-search-core.ts @@ -26,11 +26,17 @@ type CodexNativeSearchPayloadPatchResult = { status: "payload_not_object" | "native_tool_already_present" | "injected"; }; +const OPENAI_AUTH_PROVIDER_IDS = ["openai", "openai-codex"] as const; + +function isOpenAIAuthProviderId(provider: string | undefined): boolean { + return OPENAI_AUTH_PROVIDER_IDS.some((candidate) => candidate === provider); +} + export function isCodexNativeSearchEligibleModel(params: { modelProvider?: string; modelApi?: string; }): boolean { - return params.modelProvider === "openai-codex" || params.modelApi === "openai-codex-responses"; + return params.modelApi === "openai-codex-responses"; } function hasCodexNativeWebSearchTool(tools: unknown): boolean { @@ -48,7 +54,10 @@ export function hasAvailableCodexAuth(params: { }): boolean { if ( Object.values(params.config?.auth?.profiles ?? {}).some( - (profile) => isRecord(profile) && profile.provider === "openai-codex", + (profile) => + isRecord(profile) && + isOpenAIAuthProviderId(profile.provider) && + (profile.mode === "oauth" || profile.mode === "token"), ) ) { return true; @@ -56,16 +65,16 @@ export function hasAvailableCodexAuth(params: { if (params.agentDir) { try { + const store = ensureAuthProfileStore(params.agentDir, { + externalCli: externalCliDiscoveryForProviderAuth({ + cfg: params.config, + provider: "openai", + }), + }); if ( - listProfilesForProvider( - ensureAuthProfileStore(params.agentDir, { - externalCli: externalCliDiscoveryForProviderAuth({ - cfg: params.config, - provider: "openai-codex", - }), - }), - "openai-codex", - ).length > 0 + OPENAI_AUTH_PROVIDER_IDS.some( + (provider) => listProfilesForProvider(store, provider).length > 0, + ) ) { return true; } @@ -85,7 +94,10 @@ export function resolveCodexNativeSearchActivation(params: { const globalWebSearchEnabled = params.config?.tools?.web?.search?.enabled !== false; const codexConfig = resolveCodexNativeWebSearchConfig(params.config); const nativeEligible = isCodexNativeSearchEligibleModel(params); - const hasRequiredAuth = params.modelProvider !== "openai-codex" || hasAvailableCodexAuth(params); + const hasRequiredAuth = + params.modelApi !== "openai-codex-responses" || + !isOpenAIAuthProviderId(params.modelProvider) || + hasAvailableCodexAuth(params); if (!globalWebSearchEnabled) { return { diff --git a/src/agents/command/attempt-execution.cli.test.ts b/src/agents/command/attempt-execution.cli.test.ts index 1f99f3da0ff5..4fec7dc96f1e 100644 --- a/src/agents/command/attempt-execution.cli.test.ts +++ b/src/agents/command/attempt-execution.cli.test.ts @@ -27,7 +27,9 @@ vi.mock("../model-selection.js", () => ({ vi.mock("../provider-auth-aliases.js", () => ({ resolveProviderAuthAliasMap: () => ({}), resolveProviderIdForAuth: (provider: string) => - provider.trim().toLowerCase() === "codex-cli" ? "openai-codex" : provider.trim().toLowerCase(), + ["codex-cli", "openai-codex"].includes(provider.trim().toLowerCase()) + ? "openai" + : provider.trim().toLowerCase(), })); vi.mock("../model-runtime-aliases.js", async () => { @@ -1817,7 +1819,7 @@ describe("embedded attempt harness pinning", () => { }); }); - it("routes explicit OpenAI native runs with Codex OAuth through OpenClaw and the legacy Codex auth transport", async () => { + it("routes explicit OpenAI native runs with legacy Codex OAuth through OpenClaw", async () => { const sessionEntry: SessionEntry = { sessionId: "explicit-agent-codex-oauth-session", updatedAt: Date.now(), @@ -1867,7 +1869,7 @@ describe("embedded attempt harness pinning", () => { }); expectMockArgFields(runEmbeddedAgentMock, { - provider: "openai-codex", + provider: "openai", model: "gpt-5.4", agentHarnessId: "openclaw", agentHarnessRuntimeOverride: "openclaw", diff --git a/src/agents/command/attempt-execution.ts b/src/agents/command/attempt-execution.ts index 58ee0222064b..9052109b3aca 100644 --- a/src/agents/command/attempt-execution.ts +++ b/src/agents/command/attempt-execution.ts @@ -461,7 +461,7 @@ export function runAgentAttempt(params: { authProfileId: params.sessionEntry?.authProfileOverride, }) ?? params.providerOverride); const agentHarnessPolicy = isRawModelRun - ? ({ runtime: "openclaw" } as const) + ? ({ runtime: "openclaw", runtimeSource: "model" } as const) : resolveAvailableAgentHarnessPolicy({ provider: params.providerOverride, modelId: params.modelOverride, @@ -504,7 +504,7 @@ export function runAgentAttempt(params: { }); const embeddedAgentHarnessOverride = requestedAgentHarnessId ?? - (agentHarnessPolicy.runtime === "openclaw" && embeddedAgentProvider !== params.providerOverride + (agentHarnessPolicy.runtime === "openclaw" && agentHarnessPolicy.runtimeSource !== "implicit" ? "openclaw" : undefined); if (!isRawModelRun && isCliProvider(cliExecutionProvider, params.cfg)) { diff --git a/src/agents/embedded-agent-runner/compact.hooks.test.ts b/src/agents/embedded-agent-runner/compact.hooks.test.ts index cf32bee44952..9a2381939af4 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.test.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.test.ts @@ -620,11 +620,11 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => { expect(result.result?.summary).toBe("oauth fallback summary"); findMockCall( resolveModelMock, - ([provider, modelId]) => provider === "openai-codex" && modelId === "gpt-primary", + ([provider, modelId]) => provider === "openai" && modelId === "gpt-primary", ); findMockCall( resolveModelMock, - ([provider, modelId]) => provider === "openai-codex" && modelId === "gpt-fallback", + ([provider, modelId]) => provider === "openai" && modelId === "gpt-fallback", ); expectRecordFields(mockCallArg(resolveEmbeddedAgentStreamFnMock, 1), { authProfileId: "openai-codex:default", @@ -665,10 +665,10 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => { }); expect(result.ok).toBe(true); - expect(mockCallArg(resolveModelMock)).toBe("openai-codex"); + expect(mockCallArg(resolveModelMock)).toBe("openai"); expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5"); expectRecordFields(mockCallArg(resolveContextWindowInfoMock), { - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.5", }); }); @@ -704,9 +704,9 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => { expect(resolveAgentHarnessPolicyMock).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", modelId: "fake-model" }), ); - expect(mockCallArg(resolveModelMock)).toBe("openai-codex"); + expect(mockCallArg(resolveModelMock)).toBe("openai"); expectRecordFields(mockCallArg(resolveContextWindowInfoMock), { - provider: "openai-codex", + provider: "openai", modelId: "fake-model", }); }); @@ -820,10 +820,10 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => { }); expect(result.ok).toBe(true); - expect(mockCallArg(resolveModelMock)).toBe("openai-codex"); + expect(mockCallArg(resolveModelMock)).toBe("openai"); expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.4-mini"); expectRecordFields(mockCallArg(resolveContextWindowInfoMock), { - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.4-mini", }); }); @@ -861,7 +861,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => { }); expect(result.ok).toBe(true); - expect(mockCallArg(resolveModelMock)).toBe("openai-codex"); + expect(mockCallArg(resolveModelMock)).toBe("openai"); expectRecordFields(mockCallArg(resolveContextWindowInfoMock), { provider: "openai", modelId: "gpt-5.5", @@ -1836,7 +1836,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { }); }); - it("passes selected Codex runtime to queued context-engine runtime context", async () => { + it("keeps selected Codex harness queued compaction on canonical OpenAI context", async () => { resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" }); maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({ ok: true, @@ -1864,10 +1864,13 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { ); expect(result.ok).toBe(true); - const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record; - expectRecordFields(harnessArg.contextEngineRuntimeContext, { + expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled(); + const compactArg = mockCallArg(contextEngineCompactMock) as { + runtimeContext?: Record; + }; + expectRecordFields(compactArg.runtimeContext, { provider: "openai", - runtimeProvider: "openai-codex", + runtimeProvider: undefined, model: "gpt-5.5", }); }); @@ -1902,10 +1905,13 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { ); expect(result.ok).toBe(true); - const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record; - expectRecordFields(harnessArg.contextEngineRuntimeContext, { + expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled(); + const compactArg = mockCallArg(contextEngineCompactMock) as { + runtimeContext?: Record; + }; + expectRecordFields(compactArg.runtimeContext, { provider: "openai", - runtimeProvider: "openai-codex", + runtimeProvider: undefined, model: "gpt-5.5", }); }); @@ -1952,7 +1958,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { }); }); - it("keeps concrete Codex pins when explicit policy is auto for queued compaction", async () => { + it("keeps concrete Codex pins on canonical OpenAI for queued compaction", async () => { resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "auto", runtimeSource: "model", @@ -1983,10 +1989,13 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { ); expect(result.ok).toBe(true); - const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record; - expectRecordFields(harnessArg.contextEngineRuntimeContext, { + expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled(); + const compactArg = mockCallArg(contextEngineCompactMock) as { + runtimeContext?: Record; + }; + expectRecordFields(compactArg.runtimeContext, { provider: "openai", - runtimeProvider: "openai-codex", + runtimeProvider: undefined, model: "gpt-5.5", }); }); diff --git a/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts b/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts index 7051e06cebab..e51ddd85ea35 100644 --- a/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts +++ b/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts @@ -213,7 +213,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { }); }); - it("keeps configured OpenAI provider while routing Codex auth to runtime provider (#86373)", () => { + it("keeps configured OpenAI provider with legacy Codex auth profiles (#86373)", () => { const result = resolveEmbeddedCompactionTarget({ provider: "openai", modelId: "gpt-5.4", @@ -222,13 +222,13 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { defaultModel: "gpt-5.4", }); expect(result.provider).toBe("openai"); - expect(result.runtimeProvider).toBe("openai-codex"); + expect(result.runtimeProvider).toBeUndefined(); expect(result.contextProvider).toBeUndefined(); expect(result.model).toBe("gpt-5.4"); expect(result.authProfileId).toBe("openai-codex:default"); }); - it("routes openai auth order with Codex profile to openai-codex runtime provider", () => { + it("keeps openai auth order with Codex profile on canonical OpenAI", () => { const result = resolveEmbeddedCompactionTarget({ config: { auth: { order: { openai: ["openai-codex:default"] } }, @@ -239,13 +239,13 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { defaultModel: "gpt-5.5", }); expect(result.provider).toBe("openai"); - expect(result.runtimeProvider).toBe("openai-codex"); + expect(result.runtimeProvider).toBeUndefined(); expect(result.contextProvider).toBeUndefined(); expect(result.model).toBe("gpt-5.5"); expect(result.authProfileId).toBeUndefined(); }); - it("routes Codex-runtime OpenAI compaction through the plugin-backed Codex provider", () => { + it("keeps Codex-runtime OpenAI compaction on the canonical OpenAI provider", () => { const result = resolveEmbeddedCompactionTarget({ config: { models: { @@ -261,8 +261,8 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { defaultModel: "gpt-5.5", }); expect(result.provider).toBe("openai"); - expect(result.runtimeProvider).toBe("openai-codex"); - expect(result.contextProvider).toBe("openai-codex"); + expect(result.runtimeProvider).toBeUndefined(); + expect(result.contextProvider).toBeUndefined(); expect(result.model).toBe("gpt-5.5"); expect(result.authProfileId).toBeUndefined(); }); @@ -277,7 +277,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { harnessRuntime: "codex", }); expect(result.agentHarnessId).toBe("codex"); - expect(result.runtimeProvider).toBe("openai-codex"); + expect(result.runtimeProvider).toBeUndefined(); }); it("preserves direct OpenAI compaction for the OpenClaw runtime", () => { @@ -328,7 +328,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { expect(result.authProfileId).toBeUndefined(); }); - it("routes model-only compaction overrides with Codex auth through openai-codex", () => { + it("keeps model-only compaction overrides with legacy Codex auth on OpenAI", () => { const result = resolveEmbeddedCompactionTarget({ config: { agents: { defaults: { compaction: { model: "gpt-5.4" } } }, @@ -340,13 +340,13 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { defaultModel: "gpt-5.5", }); expect(result.provider).toBe("openai"); - expect(result.runtimeProvider).toBe("openai-codex"); + expect(result.runtimeProvider).toBeUndefined(); expect(result.contextProvider).toBeUndefined(); expect(result.model).toBe("gpt-5.4"); expect(result.authProfileId).toBe("openai-codex:default"); }); - it("routes openai compaction overrides with Codex auth through openai-codex", () => { + it("keeps openai compaction overrides with legacy Codex auth on OpenAI", () => { const result = resolveEmbeddedCompactionTarget({ config: { agents: { defaults: { compaction: { model: "openai/gpt-5.4" } } }, @@ -358,13 +358,13 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { defaultModel: "gpt-5.5", }); expect(result.provider).toBe("openai"); - expect(result.runtimeProvider).toBe("openai-codex"); + expect(result.runtimeProvider).toBeUndefined(); expect(result.contextProvider).toBeUndefined(); expect(result.model).toBe("gpt-5.4"); expect(result.authProfileId).toBe("openai-codex:default"); }); - it("routes OpenAI compaction model overrides through Codex runtime auth", () => { + it("keeps OpenAI compaction model overrides on canonical OpenAI with Codex runtime", () => { const result = resolveEmbeddedCompactionTarget({ config: { models: { @@ -382,8 +382,8 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { defaultModel: "gpt-5.5", }); expect(result.provider).toBe("openai"); - expect(result.runtimeProvider).toBe("openai-codex"); - expect(result.contextProvider).toBe("openai-codex"); + expect(result.runtimeProvider).toBeUndefined(); + expect(result.contextProvider).toBeUndefined(); expect(result.model).toBe("gpt-5.4-mini"); expect(result.authProfileId).toBeUndefined(); }); diff --git a/src/agents/embedded-agent-runner/extra-params.ts b/src/agents/embedded-agent-runner/extra-params.ts index a69dcd89e551..fda90ba88eee 100644 --- a/src/agents/embedded-agent-runner/extra-params.ts +++ b/src/agents/embedded-agent-runner/extra-params.ts @@ -295,6 +295,7 @@ export function resolvePreparedExtraParams(params: { workspaceDir: params.workspaceDir, provider: params.provider, modelId: params.modelId, + model: params.model, extraParams: merged, thinkingLevel: params.thinkingLevel, }, diff --git a/src/agents/embedded-agent-runner/model.forward-compat.test.ts b/src/agents/embedded-agent-runner/model.forward-compat.test.ts index b866463d4e49..97d172f4fd63 100644 --- a/src/agents/embedded-agent-runner/model.forward-compat.test.ts +++ b/src/agents/embedded-agent-runner/model.forward-compat.test.ts @@ -12,6 +12,7 @@ vi.mock("../../plugins/provider-runtime.js", () => ({ normalizeProviderResolvedModelWithPlugin: () => undefined, normalizeProviderTransportWithPlugin: () => undefined, prepareProviderDynamicModel: async () => undefined, + resolveExternalAuthProfilesWithPlugins: () => [], runProviderDynamicModel: () => undefined, shouldPreferProviderRuntimeResolvedModel: () => false, })); diff --git a/src/agents/embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts b/src/agents/embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts index 0f3b25668047..109ea144b086 100644 --- a/src/agents/embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts +++ b/src/agents/embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts @@ -40,6 +40,7 @@ vi.mock("../../plugins/provider-runtime.js", () => ({ normalizeProviderResolvedModelWithPlugin: mocks.normalizeProviderResolvedModelWithPlugin, normalizeProviderTransportWithPlugin: mocks.normalizeProviderTransportWithPlugin, prepareProviderDynamicModel: mocks.prepareProviderDynamicModel, + resolveExternalAuthProfilesWithPlugins: () => [], runProviderDynamicModel: mocks.runProviderDynamicModel, shouldPreferProviderRuntimeResolvedModel: mocks.shouldPreferProviderRuntimeResolvedModel, })); diff --git a/src/agents/embedded-agent-runner/model.startup-retry.test.ts b/src/agents/embedded-agent-runner/model.startup-retry.test.ts index 504716b4832e..8cfe8d87a952 100644 --- a/src/agents/embedded-agent-runner/model.startup-retry.test.ts +++ b/src/agents/embedded-agent-runner/model.startup-retry.test.ts @@ -37,6 +37,7 @@ vi.mock("../../plugins/provider-runtime.js", () => ({ normalizeProviderResolvedModelWithPlugin: () => undefined, normalizeProviderTransportWithPlugin: () => undefined, prepareProviderDynamicModel: async () => {}, + resolveExternalAuthProfilesWithPlugins: () => [], runProviderDynamicModel: () => undefined, shouldPreferProviderRuntimeResolvedModel: () => false, })); diff --git a/src/agents/embedded-agent-runner/model.ts b/src/agents/embedded-agent-runner/model.ts index 88e384d6106a..6d11b2597070 100644 --- a/src/agents/embedded-agent-runner/model.ts +++ b/src/agents/embedded-agent-runner/model.ts @@ -15,6 +15,8 @@ import { import { finiteSecondsToTimerSafeMilliseconds } from "../../shared/number-coercion.js"; import { discoverAuthStorage, discoverModels } from "../agent-model-discovery.js"; import { resolveDefaultAgentDir } from "../agent-scope.js"; +import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../auth-profiles.js"; +import type { AuthProfileCredential } from "../auth-profiles/types.js"; import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; import { buildModelAliasLines } from "../model-alias-lines.js"; import { resolveModelWorkspaceDir } from "../model-discovery-context.js"; @@ -25,6 +27,7 @@ import { shouldSuppressBuiltInModel, shouldUnconditionallySuppress, } from "../model-suppression.js"; +import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../openai-codex-routing.js"; import { attachModelProviderLocalService } from "../provider-local-service.js"; import { attachModelProviderRequestTransport, @@ -153,7 +156,7 @@ function discoverCachedAgentStoresForAgent( function canonicalizeLegacyResolvedModel(params: { provider: string; model: Model }): Model { if ( - normalizeProviderId(params.provider) !== "openai-codex" || + !["openai", "openai-codex"].includes(normalizeProviderId(params.provider)) || params.model.id.trim().toLowerCase() !== "gpt-5.4-codex" ) { return params.model; @@ -849,6 +852,59 @@ function resolveExplicitModelWithRegistry(params: { return undefined; } +function resolveDynamicModelAuthProfile(params: { + provider: string; + cfg?: OpenClawConfig; + agentDir?: string; + authProfileId?: string; + preferredProfile?: string; +}): { + authProfileId?: string; + authProfileMode?: AuthProfileCredential["type"] | "aws-sdk"; +} { + const explicitProfileId = params.authProfileId?.trim() || undefined; + const store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, + }); + if (explicitProfileId) { + const credential = store.profiles[explicitProfileId]; + const configuredMode = params.cfg?.auth?.profiles?.[explicitProfileId]?.mode; + return { + authProfileId: explicitProfileId, + ...(credential?.type || configuredMode + ? { authProfileMode: credential?.type ?? configuredMode } + : {}), + }; + } + const order = [ + ...new Set( + listOpenAIAuthProfileProvidersForAgentRuntime({ + provider: params.provider, + config: params.cfg, + }).flatMap((provider) => + resolveAuthProfileOrder({ + cfg: params.cfg, + store, + provider, + preferredProfile: params.preferredProfile, + }), + ), + ), + ]; + const profileId = order[0]; + if (!profileId) { + return {}; + } + const credential = store.profiles[profileId]; + const configuredMode = params.cfg?.auth?.profiles?.[profileId]?.mode; + return { + authProfileId: profileId, + ...(credential?.type || configuredMode + ? { authProfileMode: credential?.type ?? configuredMode } + : {}), + }; +} + function resolvePluginDynamicModelWithRegistry(params: { provider: string; modelId: string; @@ -856,11 +912,20 @@ function resolvePluginDynamicModelWithRegistry(params: { cfg?: OpenClawConfig; agentDir?: string; workspaceDir?: string; + authProfileId?: string; + preferredProfile?: string; runtimeHooks?: ProviderRuntimeHooks; }): Model | undefined { const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir } = params; const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS; const providerConfig = resolveConfiguredProviderConfig(cfg, provider); + const authProfile = resolveDynamicModelAuthProfile({ + provider, + cfg, + agentDir, + authProfileId: params.authProfileId, + preferredProfile: params.preferredProfile, + }); const preferDiscoveredModelMetadata = shouldCompareProviderRuntimeResolvedModel({ provider, modelId, @@ -881,6 +946,7 @@ function resolvePluginDynamicModelWithRegistry(params: { modelId, modelRegistry, providerConfig, + ...authProfile, }, }) as Model | undefined; if (!pluginDynamicModel) { @@ -1134,6 +1200,8 @@ export function resolveModelWithRegistry(params: { cfg?: OpenClawConfig; agentDir?: string; workspaceDir?: string; + authProfileId?: string; + preferredProfile?: string; runtimeHooks?: ProviderRuntimeHooks; }): Model | undefined { const workspaceDir = params.workspaceDir ?? params.cfg?.agents?.defaults?.workspace; @@ -1190,6 +1258,8 @@ export function resolveModel( runtimeHooks?: ProviderRuntimeHooks; skipProviderRuntimeHooks?: boolean; workspaceDir?: string; + authProfileId?: string; + preferredProfile?: string; }, ): { model?: Model; @@ -1218,6 +1288,8 @@ export function resolveModel( cfg, agentDir: resolvedAgentDir, workspaceDir, + authProfileId: options?.authProfileId, + preferredProfile: options?.preferredProfile, runtimeHooks, }); if (model) { @@ -1252,6 +1324,8 @@ export async function resolveModelAsync( skipProviderRuntimeHooks?: boolean; skipAgentDiscovery?: boolean; workspaceDir?: string; + authProfileId?: string; + preferredProfile?: string; }, ): Promise<{ model?: Model; @@ -1305,6 +1379,13 @@ export async function resolveModelAsync( }; } const providerConfig = resolveConfiguredProviderConfig(cfg, normalizedRef.provider); + const authProfile = resolveDynamicModelAuthProfile({ + provider: normalizedRef.provider, + cfg, + agentDir: resolvedAgentDir, + authProfileId: options?.authProfileId, + preferredProfile: options?.preferredProfile, + }); const resolveDynamicAttempt = async () => { await runtimeHooks.prepareProviderDynamicModel({ provider: normalizedRef.provider, @@ -1318,6 +1399,7 @@ export async function resolveModelAsync( modelId: normalizedRef.model, modelRegistry, providerConfig, + ...authProfile, }, }); return resolveModelWithRegistry({ @@ -1327,6 +1409,8 @@ export async function resolveModelAsync( cfg, agentDir: resolvedAgentDir, workspaceDir, + authProfileId: options?.authProfileId, + preferredProfile: options?.preferredProfile, runtimeHooks, }); }; diff --git a/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts b/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts index d5295df7cddb..470880457492 100644 --- a/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts +++ b/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts @@ -397,7 +397,7 @@ describe("runEmbeddedAgent incomplete-turn safety", () => { "strict-agentic execution contract triggered: runId=run-strict-agentic-auto-activated", ); expect(warnMessages().join("\n")).toContain( - "provider=openai-codex/gpt-5.4 harness=codex contract=strict-agentic configured=unspecified", + "provider=openai/gpt-5.4 harness=codex contract=strict-agentic configured=unspecified", ); expect(mockedLog.info.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain( "strict-agentic execution contract active", diff --git a/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts index c4319f663808..cb832c7062c9 100644 --- a/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts +++ b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts @@ -957,7 +957,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:work", }, }); @@ -995,7 +995,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { expect(mockedBuildAgentRuntimePlan).toHaveBeenCalledTimes(1); expect(pluginRunAttempt).toHaveBeenCalledTimes(1); const pluginParams = expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:work", authProfileIdSource: "user", }); @@ -1007,7 +1007,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:work", }, }); @@ -1032,14 +1032,14 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }; const runtimePlan = makeForwardedRuntimePlan({ resolvedRef: { - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.5", harnessId: "codex", }, auth: { - providerForAuth: "openai-codex", - authProfileProviderForAuth: "openai-codex", - harnessAuthProvider: "openai-codex", + providerForAuth: "openai", + authProfileProviderForAuth: "openai", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:work", }, }); @@ -1048,7 +1048,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { profiles: { "openai-codex:work": { type: "oauth" as const, - provider: "openai-codex", + provider: "openai", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, @@ -1066,7 +1066,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { mockedResolveModelAsync.mockResolvedValueOnce({ model: { id: "gpt-5.5", - provider: "openai-codex", + provider: "openai", contextWindow: 200000, api: "openai-codex-responses", }, @@ -1099,17 +1099,17 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { expect(mockedGetApiKeyForModel).toHaveBeenCalledTimes(1); expect(mockedEnsureAuthProfileStore).toHaveBeenCalledTimes(1); expectRecordFields(mockCallArg(mockedEnsureAuthProfileStore, 0, 1), { - externalCliProviderIds: ["openai-codex"], + externalCliProviderIds: ["openai"], allowKeychainPrompt: false, }); expect(mockedEnsureAuthProfileStoreWithoutExternalProfiles).not.toHaveBeenCalled(); expectMockCallFields(mockedGetApiKeyForModel, { profileId: "openai-codex:work", }); - expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenCalledWith("openai-codex", "test-key"); + expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenCalledWith("openai", "test-key"); expect(pluginRunAttempt).toHaveBeenCalledTimes(1); expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:work", authProfileIdSource: "user", resolvedApiKey: "test-key", @@ -1127,14 +1127,14 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }; const runtimePlan = makeForwardedRuntimePlan({ resolvedRef: { - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.5", harnessId: "codex", }, auth: { - providerForAuth: "openai-codex", - authProfileProviderForAuth: "openai-codex", - harnessAuthProvider: "openai-codex", + providerForAuth: "openai", + authProfileProviderForAuth: "openai", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:default", }, }); @@ -1143,7 +1143,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { profiles: { "openai-codex:default": { type: "oauth" as const, - provider: "openai-codex", + provider: "openai", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, @@ -1174,14 +1174,14 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { provider?: string; store?: { profiles?: Record }; }; - return provider === "openai-codex" && store?.profiles?.["openai-codex:default"] + return provider === "openai" && store?.profiles?.["openai-codex:default"] ? ["openai-codex:default"] : []; }); mockedResolveModelAsync.mockResolvedValueOnce({ model: { id: "gpt-5.5", - provider: "openai-codex", + provider: "openai", contextWindow: 200000, api: "openai-codex-responses", }, @@ -1224,34 +1224,34 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { expect(mockedEnsureAuthProfileStore).toHaveBeenCalledTimes(1); expectRecordFields(mockCallArg(mockedEnsureAuthProfileStore, 0, 1), { - externalCliProviderIds: ["openai-codex"], + externalCliProviderIds: ["openai"], allowKeychainPrompt: false, }); expect(mockedEnsureAuthProfileStoreWithoutExternalProfiles).not.toHaveBeenCalled(); expectMockCallFields(mockedResolveAuthProfileOrder, { - provider: "openai-codex", + provider: "openai", store: codexAuthStore, }); expect(mockedGetApiKeyForModel).toHaveBeenCalledTimes(1); expectMockCallFields(mockedGetApiKeyForModel, { profileId: "openai-codex:default", }); - expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenCalledWith("openai-codex", "test-key"); + expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenCalledWith("openai", "test-key"); expect(pluginRunAttempt).toHaveBeenCalledTimes(1); const pluginParams = expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:default", authProfileIdSource: "auto", resolvedApiKey: "test-key", }); expectRuntimePlanFields(pluginParams.runtimePlan, { resolvedRef: { - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.5", harnessId: "codex", }, auth: { - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:default", }, }); @@ -1263,7 +1263,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { const authProfiles = expectRecordFields(forwardedAuthStore.profiles, {}); expect(Object.keys(authProfiles)).toEqual(["openai-codex:default"]); expectRecordFields(authProfiles["openai-codex:default"], { - provider: "openai-codex", + provider: "openai", }); expect(harnessParams.toolAuthProfileStore).toBe(codexAuthStore); }); @@ -1291,28 +1291,28 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }; const firstRuntimePlan = makeForwardedRuntimePlan({ resolvedRef: { - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.5", harnessId: "codex", }, auth: { - providerForAuth: "openai-codex", - authProfileProviderForAuth: "openai-codex", - harnessAuthProvider: "openai-codex", + providerForAuth: "openai", + authProfileProviderForAuth: "openai", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:sub", forwardedAuthProfileCandidateIds: ["openai-codex:sub", "openai-codex:backup"], }, }); const secondRuntimePlan = makeForwardedRuntimePlan({ resolvedRef: { - provider: "openai-codex", + provider: "openai", modelId: "gpt-5.5", harnessId: "codex", }, auth: { - providerForAuth: "openai-codex", - authProfileProviderForAuth: "openai-codex", - harnessAuthProvider: "openai-codex", + providerForAuth: "openai", + authProfileProviderForAuth: "openai", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:backup", forwardedAuthProfileCandidateIds: ["openai-codex:sub", "openai-codex:backup"], }, @@ -1322,14 +1322,14 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { profiles: { "openai-codex:sub": { type: "oauth" as const, - provider: "openai-codex", + provider: "openai", access: "sub-access-token", refresh: "sub-refresh-token", expires: Date.now() + 60_000, }, "openai-codex:backup": { type: "oauth" as const, - provider: "openai-codex", + provider: "openai", access: "backup-access-token", refresh: "backup-refresh-token", expires: Date.now() + 60_000, @@ -1348,7 +1348,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { mockedResolveModelAsync.mockResolvedValueOnce({ model: { id: "gpt-5.5", - provider: "openai-codex", + provider: "openai", contextWindow: 200000, api: "openai-codex-responses", }, @@ -1394,26 +1394,18 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { } expect(mockedGetApiKeyForModel).toHaveBeenCalledTimes(2); - expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenNthCalledWith( - 1, - "openai-codex", - "sub-token", - ); - expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenNthCalledWith( - 2, - "openai-codex", - "backup-token", - ); + expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenNthCalledWith(1, "openai", "sub-token"); + expect(codexAuthStorage.setRuntimeApiKey).toHaveBeenNthCalledWith(2, "openai", "backup-token"); expect(pluginRunAttempt).toHaveBeenCalledTimes(2); expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:sub", resolvedApiKey: "sub-token", }); expectMockCallFields( pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:backup", resolvedApiKey: "backup-token", }, @@ -1434,7 +1426,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:default", }, }); @@ -1472,7 +1464,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { expect(mockedBuildAgentRuntimePlan).toHaveBeenCalledTimes(1); expect(pluginRunAttempt).toHaveBeenCalledTimes(1); const pluginParams = expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:default", authProfileIdSource: "auto", }); @@ -1484,7 +1476,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:default", }, }); @@ -1505,7 +1497,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:default", }, }); @@ -1540,12 +1532,12 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { expect(mockedGetApiKeyForModel).not.toHaveBeenCalled(); expectMockCallFields(mockedResolveAuthProfileOrder, { - provider: "openai-codex", + provider: "openai", }); expect(mockedBuildAgentRuntimePlan).toHaveBeenCalledTimes(1); expect(pluginRunAttempt).toHaveBeenCalledTimes(1); const pluginParams = expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:default", authProfileIdSource: "auto", }); @@ -1557,7 +1549,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:default", }, }); @@ -1578,7 +1570,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai:personal", }, }); @@ -1597,7 +1589,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { profiles: { "openai:personal": { type: "oauth", - provider: "openai-codex", + provider: "openai", access: "access", refresh: "refresh", expires: Date.now() + 60_000, @@ -1625,12 +1617,12 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { expect(mockedGetApiKeyForModel).not.toHaveBeenCalled(); expectMockCallFields(mockedResolveAuthProfileOrder, { - provider: "openai-codex", + provider: "openai", }); expect(mockedBuildAgentRuntimePlan).toHaveBeenCalledTimes(1); expect(pluginRunAttempt).toHaveBeenCalledTimes(1); const pluginParams = expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai:personal", authProfileIdSource: "auto", }); @@ -1642,7 +1634,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai:personal", }, }); @@ -1655,7 +1647,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { const authProfiles = expectRecordFields(authProfileStore.profiles, {}); expect(Object.keys(authProfiles)).toEqual(["openai:personal"]); expectRecordFields(authProfiles["openai:personal"], { - provider: "openai-codex", + provider: "openai", }); }); @@ -1684,7 +1676,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai-codex:sub", forwardedAuthProfileCandidateIds: ["openai-codex:sub", "openai:backup"], }, @@ -1697,7 +1689,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { }, auth: { providerForAuth: "openai", - harnessAuthProvider: "openai-codex", + harnessAuthProvider: "openai", forwardedAuthProfileId: "openai:backup", forwardedAuthProfileCandidateIds: ["openai-codex:sub", "openai:backup"], }, @@ -1719,7 +1711,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { profiles: { "openai-codex:sub": { type: "oauth", - provider: "openai-codex", + provider: "openai", access: "access", refresh: "refresh", expires: Date.now() + 60_000, @@ -1762,14 +1754,14 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => { expect(mockedGetApiKeyForModel).not.toHaveBeenCalled(); expect(pluginRunAttempt).toHaveBeenCalledTimes(2); const firstAttempt = expectMockCallFields(pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai-codex:sub", authProfileIdSource: "auto", }); const secondAttempt = expectMockCallFields( pluginRunAttempt, { - provider: "openai-codex", + provider: "openai", authProfileId: "openai:backup", authProfileIdSource: "auto", }, diff --git a/src/agents/embedded-agent-runner/run.ts b/src/agents/embedded-agent-runner/run.ts index 0bcacbd86bea..e11e9d6bb8bc 100644 --- a/src/agents/embedded-agent-runner/run.ts +++ b/src/agents/embedded-agent-runner/run.ts @@ -87,7 +87,7 @@ import { } from "../model-auth.js"; import { ensureOpenClawModelsJson } from "../models-config.js"; import { - OPENAI_CODEX_PROVIDER_ID, + OPENAI_PROVIDER_ID, listOpenAIAuthProfileProvidersForAgentRuntime, resolveContextConfigProviderForRuntime, resolveSelectedOpenAIRuntimeProvider, @@ -705,6 +705,7 @@ export async function runEmbeddedAgent( // blocking on unrelated provider discovery. skipAgentDiscovery: true, workspaceDir: resolvedWorkspace, + authProfileId: params.authProfileId, }, ); firstModelResolution ??= candidateResolution; @@ -729,6 +730,7 @@ export async function runEmbeddedAgent( params.config, { workspaceDir: resolvedWorkspace, + authProfileId: params.authProfileId, }, ); firstModelResolution ??= candidateResolution; @@ -780,17 +782,17 @@ export async function runEmbeddedAgent( const pluginHarnessNeedsOpenClawAuthBootstrap = pluginHarnessOwnsTransport && - provider === OPENAI_CODEX_PROVIDER_ID && + provider === OPENAI_PROVIDER_ID && effectiveModel.api === "openai-codex-responses"; const openClawNativeCodexResponsesNeedsAuthBootstrap = !pluginHarnessOwnsTransport && - provider === OPENAI_CODEX_PROVIDER_ID && + provider === OPENAI_PROVIDER_ID && effectiveModel.api === "openai-codex-responses"; let piExternalCliAuthScope = pluginHarnessOwnsTransport ? { ignoreAutoPreferredProfile: false } : openClawNativeCodexResponsesNeedsAuthBootstrap ? { - providerIds: [OPENAI_CODEX_PROVIDER_ID], + providerIds: [OPENAI_PROVIDER_ID], ignoreAutoPreferredProfile: false, } : resolveExternalCliAuthOverlayScopeFromSelection({ @@ -827,7 +829,7 @@ export async function runEmbeddedAgent( ? createEmptyAuthProfileStore() : pluginHarnessNeedsOpenClawAuthBootstrap ? ensureAuthProfileStore(agentDir, { - externalCliProviderIds: [OPENAI_CODEX_PROVIDER_ID], + externalCliProviderIds: [OPENAI_PROVIDER_ID], allowKeychainPrompt: false, }) : piExternalCliAuthScope.providerIds diff --git a/src/agents/embedded-agent-runner/stream-resolution.test.ts b/src/agents/embedded-agent-runner/stream-resolution.test.ts index 383c83679ab2..5981b90a9c46 100644 --- a/src/agents/embedded-agent-runner/stream-resolution.test.ts +++ b/src/agents/embedded-agent-runner/stream-resolution.test.ts @@ -82,7 +82,7 @@ describe("describeEmbeddedAgentStreamStrategy", () => { currentStreamFn: undefined, model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "codex-mini-latest", } as never, }), @@ -155,7 +155,7 @@ describe("resolveEmbeddedAgentStreamFn", () => { sessionId: "session-1", model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "codex-mini-latest", } as never, resolvedApiKey: "oauth-bearer-token", @@ -164,7 +164,7 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(streamFn).not.toBe(streamSimple); const result = await expectStreamResultRecord( streamFn( - { provider: "openai-codex", id: "codex-mini-latest" } as never, + { provider: "openai", id: "codex-mini-latest" } as never, { systemPrompt: `intro${SYSTEM_PROMPT_CACHE_BOUNDARY}tail` } as never, {}, ), @@ -407,14 +407,14 @@ describe("resolveEmbeddedAgentStreamFn", () => { sessionId: "session-1", model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.5", } as never, resolvedApiKey: "oauth-bearer-token", }); const result = await expectStreamResultRecord( - streamFn({ provider: "openai-codex", id: "gpt-5.5" } as never, {} as never, {}), + streamFn({ provider: "openai", id: "gpt-5.5" } as never, {} as never, {}), "codex api key result", ); expect(result.apiKey).toBe("oauth-bearer-token"); @@ -432,18 +432,18 @@ describe("resolveEmbeddedAgentStreamFn", () => { sessionId: "session-1", model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.5", } as never, authStorage, }); const result = await expectStreamResultRecord( - streamFn({ provider: "openai-codex", id: "gpt-5.5" } as never, {} as never, {}), + streamFn({ provider: "openai", id: "gpt-5.5" } as never, {} as never, {}), "codex stored api key result", ); expect(result.apiKey).toBe("stored-bearer-token"); - expect(authStorage.getApiKey).toHaveBeenCalledWith("openai-codex"); + expect(authStorage.getApiKey).toHaveBeenCalledWith("openai"); }); it("forwards the run abort signal into the OpenClaw native fallback when callers omit one", async () => { @@ -456,14 +456,14 @@ describe("resolveEmbeddedAgentStreamFn", () => { signal: runSignal, model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.5", } as never, resolvedApiKey: "oauth-bearer-token", }); const result = await expectStreamResultRecord( - streamFn({ provider: "openai-codex", id: "gpt-5.5" } as never, {} as never, {}), + streamFn({ provider: "openai", id: "gpt-5.5" } as never, {} as never, {}), "codex signal and api key result", ); expect(result.signal).toBe(runSignal); @@ -481,14 +481,14 @@ describe("resolveEmbeddedAgentStreamFn", () => { signal: runSignal, model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.5", } as never, resolvedApiKey: "oauth-bearer-token", }); const result = await expectStreamResultRecord( - streamFn({ provider: "openai-codex", id: "gpt-5.5" } as never, {} as never, { + streamFn({ provider: "openai", id: "gpt-5.5" } as never, {} as never, { signal: explicitSignal, }), "codex explicit signal result", @@ -506,13 +506,13 @@ describe("resolveEmbeddedAgentStreamFn", () => { signal: runSignal, model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.5", } as never, }); const result = await expectStreamResultRecord( - streamFn({ provider: "openai-codex", id: "gpt-5.5" } as never, {} as never, {}), + streamFn({ provider: "openai", id: "gpt-5.5" } as never, {} as never, {}), "codex unauthenticated signal result", ); expect(result.signal).toBe(runSignal); @@ -526,7 +526,7 @@ describe("resolveEmbeddedAgentStreamFn", () => { sessionId: "session-1", model: { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.5", } as never, resolvedApiKey: "oauth-bearer-token", @@ -534,7 +534,7 @@ describe("resolveEmbeddedAgentStreamFn", () => { const systemPrompt = `intro${SYSTEM_PROMPT_CACHE_BOUNDARY}tail`; const result = await expectStreamResultRecord( - streamFn({ provider: "openai-codex", id: "gpt-5.5" } as never, { systemPrompt } as never, {}), + streamFn({ provider: "openai", id: "gpt-5.5" } as never, { systemPrompt } as never, {}), "codex stripped context result", ); expect(result.systemPrompt).toBe("intro\ntail"); diff --git a/src/agents/embedded-agent-runner/stream-resolution.ts b/src/agents/embedded-agent-runner/stream-resolution.ts index 3452ca4196ba..df4cd3e22e49 100644 --- a/src/agents/embedded-agent-runner/stream-resolution.ts +++ b/src/agents/embedded-agent-runner/stream-resolution.ts @@ -50,7 +50,7 @@ function hasResolvedRuntimeApiKey(apiKey: string | undefined): boolean { } function isOpenAICodexResponsesModel(model: EmbeddedRunAttemptParams["model"]): boolean { - return model.provider === "openai-codex" && model.api === "openai-codex-responses"; + return model.provider === "openai" && model.api === "openai-codex-responses"; } function resolveOpenClawNativeCodexResponsesStreamFn(params: { @@ -182,7 +182,7 @@ export function resolveEmbeddedAgentStreamFn(params: { // Boundary-aware transports read credentials from options.apiKey just // like provider-owned streams, but the embedded run layer never gets to // inject the resolved runtime key for them. Without this wrap, OAuth - // providers (e.g. openai-codex/gpt-5.5) hit the Responses API with an + // providers (e.g. openai/gpt-5.5 over ChatGPT OAuth) hit the Responses API with an // empty bearer and fail with 401 Missing bearer auth header. return wrapEmbeddedAgentStreamFn(boundaryAwareStreamFn, { runSignal: params.signal, diff --git a/src/agents/execution-contract.ts b/src/agents/execution-contract.ts index 13b87b8f396a..722ece6a8308 100644 --- a/src/agents/execution-contract.ts +++ b/src/agents/execution-contract.ts @@ -39,7 +39,7 @@ const STRICT_AGENTIC_MODEL_ID_PATTERN = /^gpt-5(?:[.o-]|$)/i; * Supported provider + model combinations where strict-agentic is the intended * runtime contract. Kept as a narrow helper so both the execution-contract * resolver and the `update_plan` auto-enable gate converge on the same - * definition of "GPT-5-family openai/openai-codex run". The embedded + * definition of "GPT-5-family OpenAI run". The embedded * `mock-openai` QA lane intentionally piggybacks on that contract so repo QA * can exercise the same incomplete-turn recovery rules end to end. */ @@ -48,7 +48,7 @@ export function isStrictAgenticSupportedProviderModel(params: { modelId?: string | null; }): boolean { const provider = normalizeLowercaseStringOrEmpty(params.provider ?? ""); - if (provider !== "openai" && provider !== "openai-codex" && provider !== "mock-openai") { + if (provider !== "openai" && provider !== "mock-openai") { return false; } const modelId = typeof params.modelId === "string" ? params.modelId : ""; @@ -59,7 +59,7 @@ export function isStrictAgenticSupportedProviderModel(params: { /** * Returns the effective execution contract for an embedded OpenClaw run. * - * strict-agentic is a GPT-5-family openai/openai-codex-only runtime contract, + * strict-agentic is a GPT-5-family OpenAI-only runtime contract, * so an unsupported provider/model pair always collapses to `"default"` * regardless of what the caller passed or what config says — the contract * is inert off-provider. Within the supported lane, the behavior matrix is: @@ -71,7 +71,7 @@ export function isStrictAgenticSupportedProviderModel(params: { * - Supported provider/model + unspecified ⇒ `"strict-agentic"` so the * no-stall completion-gate criterion applies to out-of-the-box GPT-5 runs * without requiring every user to set the flag. - * - Unsupported provider/model (anything that is not openai or openai-codex + * - Unsupported provider/model (anything that is not openai * with a gpt-5-family model id) ⇒ `"default"`, even when the config * explicitly sets `"strict-agentic"`. The retry guard and blocked-exit * helpers all check this lane again, so an explicit `"strict-agentic"` @@ -94,7 +94,7 @@ export function resolveEffectiveExecutionContract(params: { agentId: params.agentId ?? undefined, }); const explicit = resolveAgentExecutionContract(params.config, sessionAgentId); - // strict-agentic is a GPT-5-family openai/openai-codex runtime contract + // strict-agentic is a GPT-5-family OpenAI runtime contract // regardless of whether it was set explicitly or auto-activated. On an // unsupported provider/model pair the contract is inert either way, so // the effective value collapses to "default". diff --git a/src/agents/gpt5-prompt-overlay.ts b/src/agents/gpt5-prompt-overlay.ts index 7530a756c282..39b396b411bb 100644 --- a/src/agents/gpt5-prompt-overlay.ts +++ b/src/agents/gpt5-prompt-overlay.ts @@ -7,9 +7,9 @@ const OPENAI_FAMILY_GPT5_PROMPT_OVERLAY_PROVIDERS = new Set([ "codex", "codex-cli", "openai", + "openai-codex", "azure-openai", "azure-openai-responses", - "openai-codex", ]); /** @deprecated OpenAI/Codex provider-owned prompt overlay helper; do not use from third-party plugins. */ diff --git a/src/agents/harness/policy.ts b/src/agents/harness/policy.ts index 0f7edebe5291..a687b1f460de 100644 --- a/src/agents/harness/policy.ts +++ b/src/agents/harness/policy.ts @@ -2,10 +2,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { AUTO_AGENT_RUNTIME_ID, type EmbeddedAgentRuntime } from "../agent-runtime-id.js"; import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; import { resolveModelRuntimePolicy } from "../model-runtime-policy.js"; -import { - isOpenAICodexProvider, - openAIProviderUsesCodexRuntimeByDefault, -} from "../openai-codex-routing.js"; +import { openAIProviderUsesCodexRuntimeByDefault } from "../openai-codex-routing.js"; export type AgentHarnessPolicy = { runtime: EmbeddedAgentRuntime; @@ -41,12 +38,6 @@ export function resolveAgentHarnessPolicy(params: { } return { runtime, runtimeSource }; } - if (isOpenAICodexProvider(params.provider)) { - if (runtime === "auto") { - return { runtime: "codex", runtimeSource }; - } - return { runtime, runtimeSource }; - } return { runtime, runtimeSource, diff --git a/src/agents/live-model-filter.test.ts b/src/agents/live-model-filter.test.ts index f983a064a262..ce3315539e60 100644 --- a/src/agents/live-model-filter.test.ts +++ b/src/agents/live-model-filter.test.ts @@ -24,14 +24,6 @@ describe("shouldExcludeProviderFromDefaultHighSignalLiveSweep", () => { resolveProviderOwners, }), ).toBe(true); - expect( - shouldExcludeProviderFromDefaultHighSignalLiveSweep({ - provider: "openai-codex", - useExplicitModels: false, - providerFilter: null, - resolveProviderOwners, - }), - ).toBe(true); expect( shouldExcludeProviderFromDefaultHighSignalLiveSweep({ provider: "codex-cli", @@ -51,22 +43,6 @@ describe("shouldExcludeProviderFromDefaultHighSignalLiveSweep", () => { resolveProviderOwners, }), ).toBe(false); - expect( - shouldExcludeProviderFromDefaultHighSignalLiveSweep({ - provider: "openai-codex", - useExplicitModels: false, - providerFilter: new Set(["codex-cli"]), - resolveProviderOwners, - }), - ).toBe(false); - expect( - shouldExcludeProviderFromDefaultHighSignalLiveSweep({ - provider: "openai-codex", - useExplicitModels: false, - providerFilter: new Set(["openai"]), - resolveProviderOwners, - }), - ).toBe(false); }); it("keeps dedicated harness providers when the caller uses explicit model selection", () => { @@ -79,7 +55,7 @@ describe("shouldExcludeProviderFromDefaultHighSignalLiveSweep", () => { ).toBe(false); }); - it("does not exclude ordinary providers", () => { + it("does not exclude ordinary or legacy OpenAI provider ids", () => { expect( shouldExcludeProviderFromDefaultHighSignalLiveSweep({ provider: "openai", @@ -88,6 +64,14 @@ describe("shouldExcludeProviderFromDefaultHighSignalLiveSweep", () => { resolveProviderOwners, }), ).toBe(false); + expect( + shouldExcludeProviderFromDefaultHighSignalLiveSweep({ + provider: "openai-codex", + useExplicitModels: false, + providerFilter: null, + resolveProviderOwners, + }), + ).toBe(false); }); }); diff --git a/src/agents/live-model-filter.ts b/src/agents/live-model-filter.ts index 211e780f1b24..1d8db75eadf2 100644 --- a/src/agents/live-model-filter.ts +++ b/src/agents/live-model-filter.ts @@ -21,7 +21,6 @@ const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [ "deepseek/deepseek-v4-pro", "minimax/minimax-m2.7", "openai/gpt-5.5", - "openai-codex/gpt-5.5", "openrouter/openai/gpt-5.2-chat", "openrouter/minimax/minimax-m2.7", "opencode-go/glm-5", @@ -44,7 +43,7 @@ const SMALL_LIVE_MODEL_PRIORITY = [ export const DEFAULT_HIGH_SIGNAL_LIVE_MODEL_LIMIT = HIGH_SIGNAL_LIVE_MODEL_PRIORITY.length; export const DEFAULT_SMALL_LIVE_MODEL_LIMIT = SMALL_LIVE_MODEL_PRIORITY.length; -const DEFAULT_HIGH_SIGNAL_LIVE_EXCLUDED_PROVIDERS = new Set(["codex", "codex-cli", "openai-codex"]); +const DEFAULT_HIGH_SIGNAL_LIVE_EXCLUDED_PROVIDERS = new Set(["codex", "codex-cli"]); const CURATED_ONLY_HIGH_SIGNAL_LIVE_PROVIDERS = new Set([ "fireworks", "google", @@ -132,7 +131,6 @@ function isOpenAiFamilyLiveModel(provider: string, id: string): boolean { } return ( provider === "openai" || - provider === "openai-codex" || provider === "codex-cli" || provider === "opencode" || provider === "github-copilot" || @@ -141,11 +139,14 @@ function isOpenAiFamilyLiveModel(provider: string, id: string): boolean { } function isUnsupportedOpenAiLiveModelRef(provider: string, id: string): boolean { + if (provider === "openai-codex") { + return true; + } if (!isOpenAiFamilyLiveModel(provider, id)) { return false; } const modelName = normalizeLowercaseStringOrEmpty(id).split("/").pop() ?? ""; - if (provider === "openai" || provider === "openai-codex") { + if (provider === "openai") { return modelName !== "gpt-5.5"; } return !modelName.startsWith("gpt-5.2"); diff --git a/src/agents/model-auth.profiles.test.ts b/src/agents/model-auth.profiles.test.ts index 5730ecb87351..6bec9c7f3c95 100644 --- a/src/agents/model-auth.profiles.test.ts +++ b/src/agents/model-auth.profiles.test.ts @@ -433,6 +433,78 @@ describe("getApiKeyForModel", () => { ); }); + it("keeps OpenAI OAuth profiles on the Codex transport and API keys on direct OpenAI", async () => { + const store = { + version: 1 as const, + profiles: { + "openai:chatgpt": { + type: "oauth" as const, + provider: "openai", + ...oauthFixture, + }, + "openai:api-key": { + type: "api_key" as const, + provider: "openai", + key: "direct-openai-key", + }, + }, + }; + + const directAuth = await getApiKeyForModel({ + model: { + id: "chat-latest", + provider: "openai", + api: "openai-responses", + } as Model, + store, + }); + const codexAuth = await getApiKeyForModel({ + model: { + id: "gpt-5.5", + provider: "openai", + api: "openai-codex-responses", + } as Model, + store, + }); + + expect(directAuth).toMatchObject({ + apiKey: "direct-openai-key", + mode: "api-key", + profileId: "openai:api-key", + }); + expect(codexAuth).toMatchObject({ + apiKey: oauthFixture.access, + mode: "oauth", + profileId: "openai:chatgpt", + }); + }); + + it("rejects an explicit OpenAI OAuth profile for direct OpenAI Platform models", async () => { + const store = { + version: 1 as const, + profiles: { + "openai:chatgpt": { + type: "oauth" as const, + provider: "openai", + ...oauthFixture, + }, + }, + }; + + await expect( + getApiKeyForModel({ + model: { + id: "chat-latest", + provider: "openai", + api: "openai-responses", + } as Model, + profileId: "openai:chatgpt", + lockedProfile: true, + store, + }), + ).rejects.toThrow(/requires an OpenAI API key profile/); + }); + it("uses the config default agent dir when resolving provider profiles", async () => { await withOpenClawTestState( { @@ -580,7 +652,7 @@ describe("getApiKeyForModel", () => { ); expect(cliCredentialMocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled(); - expect(cliCredentialMocks.readCodexCliCredentialsCached).not.toHaveBeenCalled(); + expect(cliCredentialMocks.readCodexCliCredentialsCached).toHaveBeenCalled(); expect(cliCredentialMocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled(); }); diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 75cf9e51e089..61195ddd48fa 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -69,6 +69,41 @@ export type RuntimeProviderAuthLookup = { }; const log = createSubsystemLogger("model-auth"); +const OPENAI_PROVIDER_ID = "openai"; +const OPENAI_CODEX_RESPONSES_API = "openai-codex-responses"; + +function directOpenAIPlatformModelRequiresApiKey(params: { + provider: string; + modelApi?: string; +}): boolean { + return ( + normalizeProviderId(params.provider) === OPENAI_PROVIDER_ID && + params.modelApi !== undefined && + normalizeLowercaseStringOrEmpty(params.modelApi) !== OPENAI_CODEX_RESPONSES_API + ); +} + +function isAuthModeAllowedForModel(params: { + provider: string; + modelApi?: string; + mode: ResolvedProviderAuth["mode"]; +}): boolean { + return !directOpenAIPlatformModelRequiresApiKey(params) || params.mode === "api-key"; +} + +function assertAuthModeAllowedForModel(params: { + provider: string; + modelApi?: string; + profileId: string; + mode: ResolvedProviderAuth["mode"]; +}): void { + if (isAuthModeAllowedForModel(params)) { + return; + } + throw new Error( + `Auth profile "${params.profileId}" uses ${params.mode} auth, but ${params.provider}/${params.modelApi} requires an OpenAI API key profile.`, + ); +} function resolveConfigAwareEnvApiKey( cfg: OpenClawConfig | undefined, @@ -446,17 +481,24 @@ export function hasRuntimeAvailableProviderAuth(params: { env?: NodeJS.ProcessEnv; allowPluginSyntheticAuth?: boolean; runtimeLookup?: RuntimeProviderAuthLookup; + modelApi?: string; }): boolean { const provider = normalizeProviderId(params.provider); const authOverride = resolveProviderAuthOverride(params.cfg, provider); if (authOverride === "aws-sdk") { return true; } + const envAuth = resolveEnvApiKey(provider, params.env, { + config: params.cfg, + workspaceDir: params.workspaceDir, + ...params.runtimeLookup?.envApiKey, + }); if ( - resolveEnvApiKey(provider, params.env, { - config: params.cfg, - workspaceDir: params.workspaceDir, - ...params.runtimeLookup?.envApiKey, + envAuth && + isAuthModeAllowedForModel({ + provider, + modelApi: params.modelApi, + mode: envAuth.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key", }) ) { return true; @@ -694,6 +736,12 @@ export async function resolveApiKeyForProvider(params: { source: `profile:${resolvedProfileId}`, mode: mode ? profileTypeToAuthMode(mode) : "api-key", }; + assertAuthModeAllowedForModel({ + provider, + modelApi: params.modelApi, + profileId: resolvedProfileId, + mode: result.mode, + }); // When the resolved key is a provider-owned synthetic profile marker and // the caller has not locked this profile, fall through to env/config // resolution so provider-owned real credentials take precedence. The auth @@ -767,6 +815,15 @@ export async function resolveApiKeyForProvider(params: { const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key"; + if ( + !isAuthModeAllowedForModel({ + provider, + modelApi: params.modelApi, + mode: resolvedMode, + }) + ) { + return resolveApiKeyForProvider({ ...params, credentialPrecedence: "profile-first" }); + } return { apiKey: envResolved.apiKey, source: envResolved.source, @@ -836,6 +893,15 @@ export async function resolveApiKeyForProvider(params: { source: `profile:${resolvedProfileId}`, mode: resolvedMode, }; + if ( + !isAuthModeAllowedForModel({ + provider, + modelApi: params.modelApi, + mode: result.mode, + }) + ) { + continue; + } if ( shouldDeferSyntheticProfileAuth({ cfg, @@ -859,12 +925,20 @@ export async function resolveApiKeyForProvider(params: { const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key"; - const result: ResolvedProviderAuth = { - apiKey: envResolved.apiKey, - source: envResolved.source, - mode: resolvedMode, - }; - return result; + if ( + isAuthModeAllowedForModel({ + provider, + modelApi: params.modelApi, + mode: resolvedMode, + }) + ) { + const result: ResolvedProviderAuth = { + apiKey: envResolved.apiKey, + source: envResolved.source, + mode: resolvedMode, + }; + return result; + } } const customKey = resolveUsableCustomProviderApiKey({ cfg, provider }); @@ -999,6 +1073,7 @@ export async function hasAvailableAuthForProvider(params: { store?: AuthProfileStore; agentDir?: string; workspaceDir?: string; + modelApi?: string; }): Promise { const { provider, cfg, preferredProfile } = params; @@ -1006,7 +1081,15 @@ export async function hasAvailableAuthForProvider(params: { if (authOverride === "aws-sdk") { return true; } - if (resolveConfigAwareEnvApiKey(cfg, provider, params.workspaceDir)) { + const envAuth = resolveConfigAwareEnvApiKey(cfg, provider, params.workspaceDir); + if ( + envAuth && + isAuthModeAllowedForModel({ + provider, + modelApi: params.modelApi, + mode: envAuth.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key", + }) + ) { return true; } if (resolveUsableCustomProviderApiKey({ cfg, provider })) { @@ -1040,7 +1123,15 @@ export async function hasAvailableAuthForProvider(params: { profileId: candidate, agentDir: params.agentDir, }); - if (resolved) { + const mode = resolved?.profileType ?? store.profiles[candidate]?.type; + if ( + resolved && + isAuthModeAllowedForModel({ + provider, + modelApi: params.modelApi, + mode: mode ? profileTypeToAuthMode(mode) : "api-key", + }) + ) { return true; } } catch (err) { diff --git a/src/agents/model-catalog-visibility.test.ts b/src/agents/model-catalog-visibility.test.ts index 3565a814c1b7..3d93e1d7e71b 100644 --- a/src/agents/model-catalog-visibility.test.ts +++ b/src/agents/model-catalog-visibility.test.ts @@ -37,6 +37,45 @@ describe("resolveVisibleModelCatalog", () => { expect(result).toEqual([{ provider: "openai", id: "gpt-test", name: "GPT Test" }]); }); + it("keeps Codex-routable canonical OpenAI rows visible through Codex OAuth auth", async () => { + const authChecker = vi.fn((provider: string, api?: string) => api === "openai-codex-responses"); + const catalog: ModelCatalogEntry[] = [ + { + provider: "openai", + id: "chat-latest", + name: "Chat Latest", + api: "openai-responses", + }, + { + provider: "openai", + id: "gpt-5.5", + name: "GPT 5.5", + api: "openai-responses", + }, + ]; + + const result = await resolveVisibleModelCatalog({ + cfg: {} as OpenClawConfig, + catalog, + defaultProvider: "openai", + runtimeAuthDiscovery: false, + providerAuthChecker: authChecker, + }); + + expect(authChecker).toHaveBeenNthCalledWith(1, "openai", "openai-responses"); + expect(authChecker).toHaveBeenNthCalledWith(2, "openai", "openai-responses"); + expect(authChecker).toHaveBeenNthCalledWith(3, "openai", "openai-codex-responses"); + expect(authChecker).toHaveBeenCalledTimes(3); + expect(result).toEqual([ + { + provider: "openai", + id: "gpt-5.5", + name: "GPT 5.5", + api: "openai-responses", + }, + ]); + }); + it("does not runtime-normalize unrestricted default browse", async () => { normalizeProviderModelIdWithRuntimeMock.mockImplementation(() => "custom-modern-model"); diff --git a/src/agents/model-catalog-visibility.ts b/src/agents/model-catalog-visibility.ts index b9a7fda5eb59..f56d75c2a70c 100644 --- a/src/agents/model-catalog-visibility.ts +++ b/src/agents/model-catalog-visibility.ts @@ -9,20 +9,58 @@ import { } from "./model-visibility-policy.js"; type ModelCatalogVisibilityView = "default" | "configured" | "all"; -type ProviderAuthChecker = (provider: string) => boolean | Promise; +type ProviderAuthChecker = (provider: string, modelApi?: string) => boolean | Promise; +const OPENAI_PROVIDER_ID = "openai"; +const OPENAI_CODEX_RESPONSES_API = "openai-codex-responses"; +const OPENAI_CODEX_ROUTABLE_MODEL_IDS = new Set([ + "gpt-5.5", + "gpt-5.5-pro", + "gpt-5.4", + "gpt-5.4-pro", + "gpt-5.4-mini", +]); function isPromiseLike(value: boolean | Promise): value is Promise { return typeof value === "object" && value !== null && typeof value.then === "function"; } -async function providerHasAuth( +function isCodexRoutableOpenAIPlatformCatalogEntry(entry: ModelCatalogEntry): boolean { + return ( + entry.provider.trim().toLowerCase() === OPENAI_PROVIDER_ID && + entry.api !== undefined && + entry.api !== OPENAI_CODEX_RESPONSES_API && + OPENAI_CODEX_ROUTABLE_MODEL_IDS.has(entry.id.trim().toLowerCase()) + ); +} + +async function resolveProviderAuthCheck( providerAuthChecker: ProviderAuthChecker, provider: string, + modelApi?: string, ): Promise { - const result = providerAuthChecker(provider); + const result = + modelApi === undefined + ? providerAuthChecker(provider) + : providerAuthChecker(provider, modelApi); return isPromiseLike(result) ? await result : result; } +async function providerHasAuth( + providerAuthChecker: ProviderAuthChecker, + entry: ModelCatalogEntry, +): Promise { + if (await resolveProviderAuthCheck(providerAuthChecker, entry.provider, entry.api)) { + return true; + } + return isCodexRoutableOpenAIPlatformCatalogEntry(entry) + ? await resolveProviderAuthCheck( + providerAuthChecker, + entry.provider, + OPENAI_CODEX_RESPONSES_API, + ) + : false; +} + function sortModelCatalogEntries(entries: ModelCatalogEntry[]): ModelCatalogEntry[] { return entries.toSorted( (a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id), @@ -77,7 +115,7 @@ export async function resolveVisibleModelCatalog(params: { }); const authBackedCatalog: ModelCatalogEntry[] = []; for (const entry of params.catalog) { - if (await providerHasAuth(hasAuth, entry.provider)) { + if (await providerHasAuth(hasAuth, entry)) { authBackedCatalog.push(entry); } } diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index 0418a98d8afa..5f0783ec9e3d 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -223,6 +223,7 @@ export function loadManifestModelCatalog(params: { id: row.id, name: row.name, provider: row.provider, + api: row.api, }; const contextWindow = row.contextWindow ?? row.contextTokens; if (contextWindow) { diff --git a/src/agents/model-catalog.types.ts b/src/agents/model-catalog.types.ts index 7e8a67af8c64..f7e6f85b07cb 100644 --- a/src/agents/model-catalog.types.ts +++ b/src/agents/model-catalog.types.ts @@ -1,4 +1,4 @@ -import type { ModelCompatConfig, ModelMediaInputConfig } from "../config/types.models.js"; +import type { ModelApi, ModelCompatConfig, ModelMediaInputConfig } from "../config/types.models.js"; export type ModelInputType = "text" | "image" | "audio" | "video" | "document"; @@ -7,6 +7,7 @@ export type ModelCatalogEntry = { name: string; provider: string; alias?: string; + api?: ModelApi; contextWindow?: number; contextTokens?: number; reasoning?: boolean; diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index b4b4abe09f99..3df8f504cf3b 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -508,7 +508,7 @@ describe("isHighSignalLiveModelRef", () => { false, ); expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.2" })).toBe(false); - expect(isHighSignalLiveModelRef({ provider: "openai-codex", id: "gpt-5.5" })).toBe(true); + expect(isHighSignalLiveModelRef({ provider: "openai-codex", id: "gpt-5.5" })).toBe(false); expect(isHighSignalLiveModelRef({ provider: "openai-codex", id: "gpt-5.2" })).toBe(false); expect(isHighSignalLiveModelRef({ provider: "openai-codex", id: "gpt-5.2-codex" })).toBe(false); expect(isHighSignalLiveModelRef({ provider: "openrouter", id: "openai/gpt-5.2-chat" })).toBe( @@ -660,7 +660,6 @@ describe("isPrioritizedHighSignalLiveModelRef", () => { { provider: "deepseek", id: "deepseek-v4-pro" }, { provider: "minimax", id: "minimax-m2.7" }, { provider: "openai", id: "gpt-5.5" }, - { provider: "openai-codex", id: "gpt-5.5" }, { provider: "openrouter", id: "openai/gpt-5.2-chat" }, { provider: "openrouter", id: "minimax/minimax-m2.7" }, { provider: "opencode-go", id: "glm-5" }, diff --git a/src/agents/model-provider-auth.test.ts b/src/agents/model-provider-auth.test.ts index cbdfec0a8932..da258cbb08d2 100644 --- a/src/agents/model-provider-auth.test.ts +++ b/src/agents/model-provider-auth.test.ts @@ -21,6 +21,7 @@ const modelAuthMocks = vi.hoisted(() => ({ syntheticAuthProviderRefs: [], syntheticAuthProviderRefsComplete: true, })), + hasAvailableAuthForProvider: vi.fn(() => true), hasRuntimeAvailableProviderAuth: vi.fn< (params: { @@ -49,6 +50,7 @@ vi.mock("./model-catalog.js", () => ({ vi.mock("./model-auth.js", () => ({ createRuntimeProviderAuthLookup: modelAuthMocks.createRuntimeProviderAuthLookup, + hasAvailableAuthForProvider: modelAuthMocks.hasAvailableAuthForProvider, hasRuntimeAvailableProviderAuth: modelAuthMocks.hasRuntimeAvailableProviderAuth, })); diff --git a/src/agents/model-provider-auth.ts b/src/agents/model-provider-auth.ts index 42b730ce7c0f..6f5e6decda66 100644 --- a/src/agents/model-provider-auth.ts +++ b/src/agents/model-provider-auth.ts @@ -20,6 +20,7 @@ import { } from "./auth-profiles.js"; import { createRuntimeProviderAuthLookup, + hasAvailableAuthForProvider, hasRuntimeAvailableProviderAuth, type RuntimeProviderAuthLookup, } from "./model-auth.js"; @@ -143,6 +144,7 @@ function resolveProviderAuthConfigFingerprint(cfg: OpenClawConfig | undefined): export async function hasAuthForModelProvider(params: { provider: string; + modelApi?: string; cfg?: OpenClawConfig; workspaceDir?: string; agentDir?: string; @@ -189,7 +191,8 @@ export async function hasAuthForModelProvider(params: { params.discoverExternalCliAuth !== false && params.allowPluginSyntheticAuth !== false && params.env === undefined && - params.store === undefined; + params.store === undefined && + params.modelApi === undefined; if (matchesWarmedScope) { const preparedAnswer = preparedState.providers.get(provider); if (preparedAnswer !== undefined) { @@ -205,6 +208,7 @@ export async function hasAuthForModelProvider(params: { env: params.env, allowPluginSyntheticAuth: params.allowPluginSyntheticAuth, runtimeLookup: params.runtimeAuthLookup ?? params.resolveRuntimeAuthLookup?.(), + modelApi: params.modelApi, }) ) { return true; @@ -224,7 +228,16 @@ export async function hasAuthForModelProvider(params: { externalCli: externalCliDiscoveryForProviderAuth({ cfg: params.cfg, provider }), })); if (listProfilesForProvider(store, provider).length > 0) { - return true; + return params.modelApi === undefined + ? true + : await hasAvailableAuthForProvider({ + provider, + modelApi: params.modelApi, + cfg: params.cfg, + workspaceDir: params.workspaceDir, + agentDir: slowPathAgentDir, + store, + }); } return false; } @@ -237,17 +250,19 @@ export function createProviderAuthChecker(params: { env?: NodeJS.ProcessEnv; allowPluginSyntheticAuth?: boolean; discoverExternalCliAuth?: boolean; -}): (provider: string) => Promise { +}): (provider: string, modelApi?: string) => Promise { const authCache = new Map(); let runtimeAuthLookup: RuntimeProviderAuthLookup | undefined; - return async (provider: string) => { + return async (provider: string, modelApi?: string) => { const key = normalizeProviderId(provider); - const cached = authCache.get(key); + const cacheKey = modelApi === undefined ? key : `${key}\0${modelApi}`; + const cached = authCache.get(cacheKey); if (cached !== undefined) { return cached; } const value = await hasAuthForModelProvider({ provider: key, + modelApi, cfg: params.cfg, workspaceDir: params.workspaceDir, agentDir: params.agentDir, @@ -263,7 +278,7 @@ export function createProviderAuthChecker(params: { includePluginSyntheticAuth: params.allowPluginSyntheticAuth !== false, })), }); - authCache.set(key, value); + authCache.set(cacheKey, value); return value; }; } diff --git a/src/agents/model-selection-shared.ts b/src/agents/model-selection-shared.ts index 1779342d43b5..8b7562ec6dbb 100644 --- a/src/agents/model-selection-shared.ts +++ b/src/agents/model-selection-shared.ts @@ -1193,6 +1193,7 @@ export function buildConfiguredModelCatalog(params: { provider: providerId, id, name, + api: model.api ?? provider.api, contextWindow, contextTokens, reasoning, diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index ac71973b068b..ac65fb2497f7 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -1103,10 +1103,15 @@ describe("model-selection", () => { expect(result.allowedCatalog).toEqual([ { provider: "ollama", id: "existing", name: "Existing" }, { + api: "ollama", + compat: undefined, + contextTokens: undefined, provider: "ollama", id: "glm-5.1:cloud", name: "GLM 5.1 Cloud", contextWindow: 131_072, + input: undefined, + reasoning: undefined, }, ]); expect(result.allowedKeys.has("ollama/glm-5.1:cloud")).toBe(true); diff --git a/src/agents/openai-codex-routing.test.ts b/src/agents/openai-codex-routing.test.ts index cc6cea3559f9..ec068d9919b2 100644 --- a/src/agents/openai-codex-routing.test.ts +++ b/src/agents/openai-codex-routing.test.ts @@ -9,7 +9,7 @@ import { resolveSelectedOpenAIRuntimeProvider, } from "./openai-codex-routing.js"; -describe("OpenAI Codex routing policy", () => { +describe("OpenAI runtime routing policy", () => { it("uses Codex by default for official OpenAI agent model selections", () => { expect(openAIProviderUsesCodexRuntimeByDefault({ provider: "openai" })).toBe(true); expect( @@ -43,22 +43,43 @@ describe("OpenAI Codex routing policy", () => { ).toBe("openai"); }); - it("uses Codex context config for official OpenAI under the Codex runtime", () => { + it("uses canonical OpenAI context config under the Codex runtime", () => { expect( resolveContextConfigProviderForRuntime({ provider: "openai", runtimeId: "codex", }), + ).toBe("openai"); + }); + + it("uses legacy Codex context config when canonical OpenAI config is absent", () => { + const config = { + models: { + providers: { + "openai-codex": { + baseUrl: "https://chatgpt.com/backend-api/codex", + models: [], + }, + }, + }, + } satisfies OpenClawConfig; + + expect( + resolveContextConfigProviderForRuntime({ + provider: "openai", + runtimeId: "codex", + config, + }), ).toBe("openai-codex"); }); - it("maps explicit OpenClaw plus Codex auth profile to the OpenClaw Codex-auth transport", () => { + it("keeps explicit OpenClaw plus Codex auth profile under the unified OpenAI provider", () => { expect( listOpenAIAuthProfileProvidersForAgentRuntime({ provider: "openai", harnessRuntime: "openclaw", }), - ).toEqual(["openai", "openai-codex"]); + ).toEqual(["openai-codex", "openai"]); expect( resolveOpenAIRuntimeProvider({ provider: "openai", @@ -66,10 +87,10 @@ describe("OpenAI Codex routing policy", () => { authProfileProvider: "openai-codex", authProfileId: "openai-codex:work", }), - ).toBe("openai-codex"); + ).toBe("openai"); }); - it("keeps explicit OpenAI OpenClaw Codex auth order ahead of API-key backups", () => { + it("keeps legacy Codex auth order under the canonical OpenAI provider", () => { const config = { auth: { order: { @@ -91,7 +112,7 @@ describe("OpenAI Codex routing policy", () => { harnessRuntime: "openclaw", config, }), - ).toBe("openai-codex"); + ).toBe("openai"); expect( resolveOpenAIRuntimeProvider({ provider: "openai", @@ -101,6 +122,24 @@ describe("OpenAI Codex routing policy", () => { ).toBe("openai"); }); + it("checks legacy Codex auth before canonical OpenAI for pre-doctor state", () => { + const config = { + auth: { + order: { + "openai-codex": ["openai-codex:work", "openai:backup"], + }, + }, + } satisfies OpenClawConfig; + + expect( + listOpenAIAuthProfileProvidersForAgentRuntime({ + provider: "openai", + harnessRuntime: "openclaw", + config, + }), + ).toEqual(["openai-codex", "openai"]); + }); + it("keeps explicit OpenAI OpenClaw API-key auth order ahead of Codex backups", () => { const config = { auth: { @@ -116,7 +155,7 @@ describe("OpenAI Codex routing policy", () => { harnessRuntime: "openclaw", config, }), - ).toEqual(["openai", "openai-codex"]); + ).toEqual(["openai-codex", "openai"]); expect( resolveSelectedOpenAIRuntimeProvider({ provider: "openai", @@ -149,7 +188,7 @@ describe("OpenAI Codex routing policy", () => { harnessRuntime: "openclaw", config, }), - ).toEqual(["openai", "openai-codex"]); + ).toEqual(["openai"]); expect( resolveSelectedOpenAIRuntimeProvider({ provider: "openai", @@ -159,22 +198,22 @@ describe("OpenAI Codex routing policy", () => { ).toBe("openai"); }); - it("validates Codex harness auth through the Codex provider contract", () => { + it("validates Codex harness auth through the unified OpenAI provider contract", () => { expect( listOpenAIAuthProfileProvidersForAgentRuntime({ provider: "openai", harnessRuntime: "codex", }), - ).toEqual(["openai-codex"]); + ).toEqual(["openai-codex", "openai"]); }); - it("routes openai provider to openai-codex when harness runtime is codex", () => { + it("keeps OpenAI as the runtime provider when harness runtime is codex", () => { expect( resolveSelectedOpenAIRuntimeProvider({ provider: "openai", harnessRuntime: "codex", }), - ).toBe("openai-codex"); + ).toBe("openai"); }); it("does not route non-OpenAI providers when runtime is codex", () => { diff --git a/src/agents/openai-codex-routing.ts b/src/agents/openai-codex-routing.ts index 8f0a41c9a2a8..9f851d1d5193 100644 --- a/src/agents/openai-codex-routing.ts +++ b/src/agents/openai-codex-routing.ts @@ -1,9 +1,5 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; -import { OPENCLAW_AGENT_RUNTIME_ID } from "./agent-runtime-id.js"; -import { normalizeOptionalAgentRuntimeId } from "./agent-runtime-id.js"; -import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; -import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js"; +import { normalizeProviderId } from "./provider-id.js"; export const OPENAI_PROVIDER_ID = "openai"; export const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; @@ -31,8 +27,13 @@ function openAIProviderUsesCustomBaseUrl(config: OpenClawConfig | undefined): bo return !isOfficialOpenAIBaseUrl(config?.models?.providers?.openai?.baseUrl); } +function hasProviderConfig(config: OpenClawConfig | undefined, provider: string): boolean { + return Boolean(config?.models?.providers?.[provider]); +} + export function isOpenAIProvider(provider: string | undefined): boolean { - return normalizeProviderId(provider ?? "") === OPENAI_PROVIDER_ID; + const normalized = normalizeProviderId(provider ?? ""); + return normalized === OPENAI_PROVIDER_ID || normalized === OPENAI_CODEX_PROVIDER_ID; } export function isOpenAICodexProvider(provider: string | undefined): boolean { @@ -72,59 +73,6 @@ export function modelSelectionShouldEnsureCodexPlugin(params: { return provider === OPENAI_PROVIDER_ID && !openAIProviderUsesCustomBaseUrl(params.config); } -export function hasOpenAICodexAuthProfileOverride(value: unknown): boolean { - return ( - typeof value === "string" && - normalizeOptionalLowercaseString(value)?.startsWith(`${OPENAI_CODEX_PROVIDER_ID}:`) === true - ); -} - -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 shouldRouteOpenAIThroughCodexAuthProvider(params: { - provider: string; - harnessRuntime?: string; - agentHarnessId?: string; - authProfileProvider?: string; - authProfileId?: string; - config?: OpenClawConfig; - workspaceDir?: string; -}): boolean { - if (!isOpenAIProvider(params.provider)) { - return false; - } - const runtime = - normalizeOptionalAgentRuntimeId(params.agentHarnessId ?? params.harnessRuntime) ?? - OPENCLAW_AGENT_RUNTIME_ID; - if (runtime !== "openclaw") { - return false; - } - if (!hasOpenAICodexAuthProfileOverride(params.authProfileId)) { - return false; - } - const aliasLookupParams = { - config: params.config, - workspaceDir: params.workspaceDir, - }; - const authProfileProvider = resolveProviderIdForAuth( - params.authProfileProvider ?? params.authProfileId?.split(":", 1)[0] ?? "", - aliasLookupParams, - ); - return authProfileProvider === OPENAI_CODEX_PROVIDER_ID; -} - export function listOpenAIAuthProfileProvidersForAgentRuntime(params: { provider: string; harnessRuntime?: string; @@ -134,25 +82,12 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: { if (!isOpenAIProvider(params.provider)) { return [params.provider]; } - const runtime = - normalizeOptionalAgentRuntimeId( - normalizeExplicitRuntimePin(params.agentHarnessId) ?? params.harnessRuntime, - ) ?? OPENCLAW_AGENT_RUNTIME_ID; - if (runtime === "codex") { - return [OPENAI_CODEX_PROVIDER_ID]; - } - if (runtime === "openclaw") { - if (configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)) { - return [OPENAI_CODEX_PROVIDER_ID, OPENAI_PROVIDER_ID]; - } - return [OPENAI_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID]; - } - return [params.provider]; -} - -function normalizeExplicitRuntimePin(value: unknown): string | undefined { - const runtime = normalizeOptionalAgentRuntimeId(value); - return runtime === "auto" || runtime === "default" ? undefined : runtime; + return openAIProviderUsesCodexRuntimeByDefault({ + provider: params.provider, + config: params.config, + }) + ? [OPENAI_CODEX_PROVIDER_ID, OPENAI_PROVIDER_ID] + : [OPENAI_PROVIDER_ID]; } export function resolveOpenAIRuntimeProvider(params: { @@ -164,9 +99,7 @@ export function resolveOpenAIRuntimeProvider(params: { config?: OpenClawConfig; workspaceDir?: string; }): string { - return shouldRouteOpenAIThroughCodexAuthProvider(params) - ? OPENAI_CODEX_PROVIDER_ID - : params.provider; + return isOpenAIProvider(params.provider) ? OPENAI_PROVIDER_ID : params.provider; } export function resolveSelectedOpenAIRuntimeProvider(params: { @@ -178,23 +111,7 @@ export function resolveSelectedOpenAIRuntimeProvider(params: { config?: OpenClawConfig; workspaceDir?: string; }): string { - if (shouldRouteOpenAIThroughCodexAuthProvider(params)) { - return OPENAI_CODEX_PROVIDER_ID; - } - const runtime = - normalizeOptionalAgentRuntimeId(params.agentHarnessId ?? params.harnessRuntime) ?? - OPENCLAW_AGENT_RUNTIME_ID; - if (!isOpenAIProvider(params.provider)) { - return params.provider; - } - if (runtime === "codex") { - return OPENAI_CODEX_PROVIDER_ID; - } - return runtime === "openclaw" && - !params.authProfileId?.trim() && - configuredOpenAIAuthOrderStartsWithCodexProfile(params.config) - ? OPENAI_CODEX_PROVIDER_ID - : params.provider; + return isOpenAIProvider(params.provider) ? OPENAI_PROVIDER_ID : params.provider; } export function resolveContextConfigProviderForRuntime(params: { @@ -202,14 +119,15 @@ export function resolveContextConfigProviderForRuntime(params: { runtimeId?: string; config?: OpenClawConfig; }): string { - const provider = normalizeProviderId(params.provider); - const runtimeId = normalizeOptionalAgentRuntimeId(params.runtimeId) ?? OPENCLAW_AGENT_RUNTIME_ID; + if (!isOpenAIProvider(params.provider)) { + return params.provider; + } if ( - provider === OPENAI_PROVIDER_ID && - runtimeId === "codex" && - openAIProviderUsesCodexRuntimeByDefault({ provider, config: params.config }) + params.runtimeId === "codex" && + !hasProviderConfig(params.config, OPENAI_PROVIDER_ID) && + hasProviderConfig(params.config, OPENAI_CODEX_PROVIDER_ID) ) { return OPENAI_CODEX_PROVIDER_ID; } - return params.provider; + return OPENAI_PROVIDER_ID; } diff --git a/src/agents/openai-reasoning-compat.ts b/src/agents/openai-reasoning-compat.ts index a97e9095963b..66705bb2004b 100644 --- a/src/agents/openai-reasoning-compat.ts +++ b/src/agents/openai-reasoning-compat.ts @@ -32,8 +32,7 @@ export function resolveOpenAIReasoningEffortMap( const provider = normalizeLowercaseStringOrEmpty(model.provider ?? ""); const id = normalizeLowercaseStringOrEmpty(model.id ?? ""); const builtinMap: Record = - (provider === "openai" || provider === "openai-codex") && - OPENAI_MEDIUM_ONLY_REASONING_MODEL_IDS.has(id) + provider === "openai" && OPENAI_MEDIUM_ONLY_REASONING_MODEL_IDS.has(id) ? { minimal: "medium", low: "medium" } : {}; return { diff --git a/src/agents/openai-reasoning-effort.ts b/src/agents/openai-reasoning-effort.ts index 662225bdd485..fe80a27f44cb 100644 --- a/src/agents/openai-reasoning-effort.ts +++ b/src/agents/openai-reasoning-effort.ts @@ -65,9 +65,6 @@ export function resolveOpenAISupportedReasoningEfforts( return compatEfforts; } - const provider = normalizeLowercaseStringOrEmpty( - typeof model.provider === "string" ? model.provider : "", - ); const id = normalizeModelId(typeof model.id === "string" ? model.id : undefined); if (id === "gpt-5.1-codex-mini") { return GPT_51_CODEX_MINI_REASONING_EFFORTS; @@ -75,7 +72,7 @@ export function resolveOpenAISupportedReasoningEfforts( if (id === "gpt-5.1-codex-max") { return GPT_51_CODEX_MAX_REASONING_EFFORTS; } - if (/^gpt-5(?:\.\d+)?-codex(?:-|$)/u.test(id) || provider === "openai-codex") { + if (/^gpt-5(?:\.\d+)?-codex(?:-|$)/u.test(id)) { return GPT_CODEX_REASONING_EFFORTS; } if (id === "gpt-5-pro") { diff --git a/src/agents/openai-responses-payload-policy.ts b/src/agents/openai-responses-payload-policy.ts index b821e2dd7916..d47575cd303c 100644 --- a/src/agents/openai-responses-payload-policy.ts +++ b/src/agents/openai-responses-payload-policy.ts @@ -223,6 +223,7 @@ function resolveOpenAIResponsesPayloadCapabilities( ): OpenAIResponsesPayloadCapabilities { const provider = normalizeLowercaseString(model.provider); const api = normalizeLowercaseString(model.api); + const isOpenAIProvider = provider === "openai" || provider === "openai-codex"; const endpointClass = resolveBundledOpenAIResponsesEndpointClass(model.baseUrl); const isResponsesApi = isOpenAIResponsesApi(api); const usesConfiguredBaseUrl = endpointClass !== "default"; @@ -248,13 +249,14 @@ function resolveOpenAIResponsesPayloadCapabilities( (provider === "openai" && (api === "openai-responses" || api === "openclaw-openai-responses-transport") && endpointClass === "openai-public") || - (provider === "openai-codex" && + (isOpenAIProvider && (api === "openai-codex-responses" || api === "openai-responses" || api === "openclaw-openai-responses-transport") && endpointClass === "openai-codex"), allowsResponsesStore: supportsResponsesStoreField && + api !== "openai-codex-responses" && provider !== undefined && OPENAI_RESPONSES_PROVIDERS.has(provider) && usesKnownNativeOpenAIEndpoint, diff --git a/src/agents/openai-strict-tool-setting.ts b/src/agents/openai-strict-tool-setting.ts index 1568f077486c..44623a7a476c 100644 --- a/src/agents/openai-strict-tool-setting.ts +++ b/src/agents/openai-strict-tool-setting.ts @@ -31,7 +31,6 @@ function resolvesToNativeOpenAIStrictTools( } return ( capabilities.provider === "openai" || - capabilities.provider === "openai-codex" || capabilities.provider === "azure-openai" || capabilities.provider === "azure-openai-responses" ); diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index d1c3c0ac2596..e0b1a40a0fec 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -491,7 +491,7 @@ describe("openai transport stream", () => { id: "gpt-5.4-codex", name: "GPT-5.4 Codex", api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", baseUrl: "https://chatgpt.com/backend-api", headers: { originator: "openclaw", @@ -2323,6 +2323,36 @@ describe("openai transport stream", () => { expect(params.prompt_cache_retention).toBeUndefined(); }); + it("treats canonical OpenAI Codex responses models as native Codex responses", () => { + const params = buildOpenAIResponsesParams( + { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-codex-responses", + provider: "openai", + baseUrl: "https://chatgpt.com/backend-api/codex", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, + { + systemPrompt: "", + messages: [{ role: "user", content: "Reply OK", timestamp: 1 }], + tools: [], + } as never, + { + maxTokens: 16, + sessionId: "session-123", + }, + ) as Record; + + expect(params.instructions).toBe("Follow the user request."); + expect(params.max_output_tokens).toBeUndefined(); + expect(params.prompt_cache_retention).toBeUndefined(); + }); + it("does not add fallback instructions for custom Codex-compatible responses backends", () => { const params = buildOpenAIResponsesParams( { diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index 655eafe37436..09701fc79db4 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -90,6 +90,7 @@ const MAX_OPENAI_STRICT_TOOL_DOWNGRADE_DIAGNOSTIC_KEYS = 256; const OPENAI_RESPONSES_REASONING_REPLAY_META_KEY = "__openclaw_replay"; const OPENAI_RESPONSES_REASONING_REPLAY_BLOCK_META_KEY = "openclawReasoningReplay"; const OPENAI_RESPONSES_REPLAY_ITEM_ID_MAX_LENGTH = 64; +const OPENAI_CODEX_RESPONSES_PROVIDERS = new Set(["openai", "openai-codex"]); const log = createSubsystemLogger("openai-transport"); const loggedOpenAIStrictToolDowngradeDiagnosticKeys = new Set(); @@ -1916,7 +1917,7 @@ function raiseMinimalReasoningForResponsesWebSearch(params: { function isOpenAICodexResponsesModel(model: Model): boolean { return ( - model.provider === "openai-codex" && + OPENAI_CODEX_RESPONSES_PROVIDERS.has(model.provider) && (model.api === "openai-codex-responses" || model.api === "openclaw-openai-responses-transport") ); } diff --git a/src/agents/openclaw-tools.update-plan.test.ts b/src/agents/openclaw-tools.update-plan.test.ts index 65e5c87c3c9c..2074d5c03961 100644 --- a/src/agents/openclaw-tools.update-plan.test.ts +++ b/src/agents/openclaw-tools.update-plan.test.ts @@ -238,7 +238,6 @@ describe("openclaw-tools update_plan gating", () => { } as OpenClawConfig; expectUpdatePlanEnabled(openAiGpt5Params(cfg), true); - expectUpdatePlanEnabled(openAiGpt5Params(cfg, { modelProvider: "openai-codex" }), true); }); it("respects explicit default contract opt-out on GPT-5 runs", () => { diff --git a/src/agents/prompt-overlay-runtime-contract.test.ts b/src/agents/prompt-overlay-runtime-contract.test.ts index 1a412be2e407..664d16666359 100644 --- a/src/agents/prompt-overlay-runtime-contract.test.ts +++ b/src/agents/prompt-overlay-runtime-contract.test.ts @@ -4,7 +4,6 @@ import { NON_GPT5_CONTRACT_MODEL_ID, NON_OPENAI_CONTRACT_PROVIDER_ID, CODEX_CONTRACT_PROVIDER_ID, - OPENAI_CODEX_CONTRACT_PROVIDER_ID, OPENAI_CONTRACT_PROVIDER_ID, openAiPluginPersonalityConfig, sharedGpt5PersonalityConfig, @@ -53,7 +52,7 @@ describe("GPT-5 prompt overlay runtime contract", () => { it("scopes OpenAI plugin personality fallback to OpenAI-family GPT-5 providers", () => { const openAiContribution = resolveGpt5SystemPromptContribution({ - providerId: OPENAI_CODEX_CONTRACT_PROVIDER_ID, + providerId: OPENAI_CONTRACT_PROVIDER_ID, modelId: GPT5_CONTRACT_MODEL_ID, config: openAiPluginPersonalityConfig("off"), }); diff --git a/src/agents/provider-attribution.test.ts b/src/agents/provider-attribution.test.ts index 6285fec3986c..05784da7ceda 100644 --- a/src/agents/provider-attribution.test.ts +++ b/src/agents/provider-attribution.test.ts @@ -197,16 +197,16 @@ describe("provider attribution", () => { }); }); - it("returns a hidden-spec OpenAI Codex attribution policy", () => { + it("maps legacy OpenAI Codex attribution to canonical OpenAI policy", () => { expect( resolveProviderAttributionPolicy("openai-codex", { OPENCLAW_VERSION: "2026.3.22" }), ).toEqual({ - provider: "openai-codex", + provider: "openai", enabledByDefault: true, verification: "vendor-hidden-api-spec", hook: "request-headers", reviewNote: - "OpenAI Codex ChatGPT-backed traffic supports the same hidden originator/User-Agent attribution contract.", + "OpenAI native traffic supports hidden originator/User-Agent attribution. Verified against the Codex wire contract.", product: "OpenClaw", version: "2026.3.22", headers: { @@ -252,7 +252,6 @@ describe("provider attribution", () => { ["openrouter", true, "vendor-documented", "request-headers"], ["nvidia", true, "vendor-documented", "request-headers"], ["openai", true, "vendor-hidden-api-spec", "request-headers"], - ["openai-codex", true, "vendor-hidden-api-spec", "request-headers"], ["xai", true, "vendor-hidden-api-spec", "request-headers"], ["anthropic", false, "vendor-sdk-hook-only", "default-headers"], ["google", false, "vendor-sdk-hook-only", "user-agent-extra"], @@ -403,7 +402,7 @@ describe("provider attribution", () => { }), { endpointClass: "openai-codex", - attributionProvider: "openai-codex", + attributionProvider: "openai", allowsHiddenAttribution: true, }, ); @@ -902,6 +901,24 @@ describe("provider attribution", () => { shouldStripResponsesPromptCache: false, }, ); + expectRecordFields( + resolveProviderRequestCapabilities({ + provider: "openai-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + capability: "llm", + transport: "stream", + }), + { + endpointClass: "openai-codex", + attributionProvider: "openai", + allowsOpenAIServiceTier: true, + supportsOpenAIReasoningCompatPayload: true, + allowsResponsesStore: true, + supportsResponsesStoreField: true, + shouldStripResponsesPromptCache: false, + }, + ); expectRecordFields( resolveProviderRequestCapabilities({ @@ -1297,7 +1314,7 @@ describe("provider attribution", () => { isKnownNativeEndpoint: true, allowsOpenAIServiceTier: true, supportsOpenAIReasoningCompatPayload: true, - allowsResponsesStore: false, + allowsResponsesStore: true, supportsResponsesStoreField: true, shouldStripResponsesPromptCache: false, allowsAnthropicServiceTier: false, diff --git a/src/agents/provider-attribution.ts b/src/agents/provider-attribution.ts index 79fc0d7b9b82..2b0c3d73200e 100644 --- a/src/agents/provider-attribution.ts +++ b/src/agents/provider-attribution.ts @@ -136,7 +136,12 @@ const OPENAI_RESPONSES_APIS = new Set([ "azure-openai-responses", "openai-codex-responses", ]); -const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]); +const OPENAI_RESPONSES_PROVIDERS = new Set([ + "openai", + "openai-codex", + "azure-openai", + "azure-openai-responses", +]); const MANIFEST_PROVIDER_ENDPOINT_CLASSES = new Set([ "anthropic-public", "cerebras-native", @@ -446,6 +451,10 @@ function isOpenAIResponsesApi(api: string | null | undefined): boolean { return normalizedApi !== undefined && OPENAI_RESPONSES_APIS.has(normalizedApi); } +function isCanonicalOrLegacyOpenAIProvider(provider: string | undefined): boolean { + return provider === "openai" || provider === "openai-codex"; +} + export function resolveProviderAttributionIdentity( env: RuntimeVersionEnv = process.env as RuntimeVersionEnv, ): ProviderAttributionIdentity { @@ -512,26 +521,6 @@ function buildOpenAIAttributionPolicy( }; } -function buildOpenAICodexAttributionPolicy( - env: RuntimeVersionEnv = process.env as RuntimeVersionEnv, -): ProviderAttributionPolicy { - const identity = resolveProviderAttributionIdentity(env); - return { - provider: "openai-codex", - enabledByDefault: true, - verification: "vendor-hidden-api-spec", - hook: "request-headers", - reviewNote: - "OpenAI Codex ChatGPT-backed traffic supports the same hidden originator/User-Agent attribution contract.", - ...identity, - headers: { - originator: OPENCLAW_ATTRIBUTION_ORIGINATOR, - version: identity.version, - "User-Agent": formatOpenClawUserAgent(identity.version), - }, - }; -} - function buildXaiAttributionPolicy( env: RuntimeVersionEnv = process.env as RuntimeVersionEnv, ): ProviderAttributionPolicy { @@ -575,7 +564,6 @@ export function listProviderAttributionPolicies( buildOpenRouterAttributionPolicy(env), buildNvidiaAttributionPolicy(env), buildOpenAIAttributionPolicy(env), - buildOpenAICodexAttributionPolicy(env), buildXaiAttributionPolicy(env), buildSdkHookOnlyPolicy( "anthropic", @@ -615,7 +603,8 @@ export function resolveProviderAttributionPolicy( env: RuntimeVersionEnv = process.env as RuntimeVersionEnv, ): ProviderAttributionPolicy | undefined { const normalized = normalizeProviderId(provider ?? ""); - return listProviderAttributionPolicies(env).find((policy) => policy.provider === normalized); + const canonical = normalized === "openai-codex" ? "openai" : normalized; + return listProviderAttributionPolicies(env).find((policy) => policy.provider === canonical); } export function resolveProviderAttributionHeaders( @@ -650,10 +639,8 @@ export function resolveProviderRequestPolicy( const usesExplicitProxyLikeEndpoint = usesConfiguredBaseUrl && !usesKnownNativeOpenAIEndpoint; let attributionProvider: string | undefined; - if (provider === "openai" && usesOpenAIPublicAttributionHost) { + if (isCanonicalOrLegacyOpenAIProvider(provider) && usesVerifiedOpenAIAttributionHost) { attributionProvider = "openai"; - } else if (provider === "openai-codex" && usesOpenAICodexAttributionHost) { - attributionProvider = "openai-codex"; } else if (provider === "openrouter" && policy?.enabledByDefault) { // OpenRouter attribution is documented, but only apply it to known // OpenRouter endpoints or the default (unset) baseUrl path. @@ -690,7 +677,9 @@ export function resolveProviderRequestPolicy( attributionPolicy?.verification === "vendor-hidden-api-spec", usesKnownNativeOpenAIEndpoint, usesKnownNativeOpenAIRoute: - endpointClass === "default" ? provider === "openai" : usesKnownNativeOpenAIEndpoint, + endpointClass === "default" + ? isCanonicalOrLegacyOpenAIProvider(provider) + : usesKnownNativeOpenAIEndpoint, usesVerifiedOpenAIAttributionHost, usesExplicitProxyLikeEndpoint, }; @@ -757,16 +746,17 @@ export function resolveProviderRequestCapabilities( ...policy, isKnownNativeEndpoint, allowsOpenAIServiceTier: - (provider === "openai" && api === "openai-responses" && endpointClass === "openai-public") || - (provider === "openai-codex" && + (isCanonicalOrLegacyOpenAIProvider(provider) && + api === "openai-responses" && + endpointClass === "openai-public") || + (isCanonicalOrLegacyOpenAIProvider(provider) && (api === "openai-codex-responses" || api === "openai-responses") && endpointClass === "openai-codex"), supportsOpenAIReasoningCompatPayload: provider !== undefined && api !== undefined && !policy.usesExplicitProxyLikeEndpoint && - (provider === "openai" || - provider === "openai-codex" || + (isCanonicalOrLegacyOpenAIProvider(provider) || provider === "azure-openai" || provider === "azure-openai-responses") && (api === "openai-completions" || diff --git a/src/agents/provider-transport-fetch.test.ts b/src/agents/provider-transport-fetch.test.ts index 8c28fc3d0095..02a0224e57de 100644 --- a/src/agents/provider-transport-fetch.test.ts +++ b/src/agents/provider-transport-fetch.test.ts @@ -1208,6 +1208,23 @@ describe("buildGuardedModelFetch", () => { }, ); + it("ignores invalid obsolete asctime retry-after values", async () => { + fetchWithSsrFGuardMock.mockResolvedValue({ + response: new Response(null, { + status: 503, + headers: { "retry-after": "Sun Nov 99 99:99:99 9999" }, + }), + finalUrl: "https://api.anthropic.com/v1/messages", + release: vi.fn(async () => undefined), + }); + const response = await buildGuardedModelFetch(anthropicModel)( + "https://api.anthropic.com/v1/messages", + { method: "POST" }, + ); + + expect(response.headers.get("x-should-retry")).toBeNull(); + }); + it("respects OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS", async () => { process.env.OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS = "10"; fetchWithSsrFGuardMock.mockResolvedValue({ diff --git a/src/agents/provider-transport-fetch.ts b/src/agents/provider-transport-fetch.ts index 08089cbeca06..8693ee6e72c7 100644 --- a/src/agents/provider-transport-fetch.ts +++ b/src/agents/provider-transport-fetch.ts @@ -42,6 +42,13 @@ const BLOCKED_EXACT_ORIGIN_TRUST_HOSTNAME_LABELS = new Set(["instance-data"]); const PLAIN_DECIMAL_NUMBER_RE = /^\d+(?:\.\d+)?$/; const RETRY_AFTER_HTTP_DATE_RE = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2} \d{2}:\d{2}:\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ \d]\d \d{2}:\d{2}:\d{2} \d{4})$/; +const HTTP_DATE_MONTH_INDEX = new Map( + ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].map( + (month, index) => [month, index], + ), +); +const OBSOLETE_ASCTIME_HTTP_DATE_RE = + /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/; function hasReadableSseData(block: string): boolean { const dataLines = block @@ -268,7 +275,7 @@ function parseRetryAfterSeconds(headers: Headers): number | undefined { return undefined; } - const retryAt = Date.parse(trimmedRetryAfter); + const retryAt = parseRetryAfterHttpDateMs(trimmedRetryAfter); if (Number.isNaN(retryAt)) { return undefined; } @@ -276,6 +283,47 @@ function parseRetryAfterSeconds(headers: Headers): number | undefined { return Math.max(0, (retryAt - Date.now()) / 1000); } +function parseRetryAfterHttpDateMs(value: string): number { + const match = OBSOLETE_ASCTIME_HTTP_DATE_RE.exec(value); + if (match) { + const month = HTTP_DATE_MONTH_INDEX.get(match[1] ?? ""); + if (month === undefined) { + return Number.NaN; + } + const year = Number.parseInt(match[6] ?? "", 10); + const day = Number.parseInt((match[2] ?? "").trim(), 10); + const hours = Number.parseInt(match[3] ?? "", 10); + const minutes = Number.parseInt(match[4] ?? "", 10); + const seconds = Number.parseInt(match[5] ?? "", 10); + if ( + day < 1 || + day > 31 || + hours > 23 || + minutes > 59 || + seconds > 59 || + [year, day, hours, minutes, seconds].some((component) => !Number.isFinite(component)) + ) { + return Number.NaN; + } + const timestamp = Date.UTC(year, month, day, hours, minutes, seconds); + const parsedDate = new Date(timestamp); + return parsedDate.getUTCFullYear() === year && + parsedDate.getUTCMonth() === month && + parsedDate.getUTCDate() === day && + parsedDate.getUTCHours() === hours && + parsedDate.getUTCMinutes() === minutes && + parsedDate.getUTCSeconds() === seconds + ? timestamp + : Number.NaN; + } + + const parsed = Date.parse(value); + if (!Number.isNaN(parsed)) { + return parsed; + } + return Number.NaN; +} + function resolveMaxSdkRetryWaitSeconds(): number | undefined { const raw = process.env.OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS?.trim(); if (!raw) { diff --git a/src/agents/runtime-plan/auth.ts b/src/agents/runtime-plan/auth.ts index 630d56fa4a9b..8875dbbcea82 100644 --- a/src/agents/runtime-plan/auth.ts +++ b/src/agents/runtime-plan/auth.ts @@ -1,11 +1,9 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; -import { shouldRouteOpenAIThroughCodexAuthProvider } from "../openai-codex-routing.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import type { AgentRuntimeAuthPlan } from "./types.js"; -const CODEX_HARNESS_AUTH_PROVIDER = "openai-codex"; -const OPENAI_PROVIDER = "openai"; +const CODEX_HARNESS_AUTH_PROVIDER = "openai"; function resolveHarnessAuthProvider(params: { harnessId?: string; @@ -44,23 +42,10 @@ export function buildAgentRuntimeAuthPlan(params: { const harnessCanForwardProfile = params.allowHarnessAuthProfileForwarding !== false && harnessProviderForAuth && - (harnessProviderForAuth === authProfileProviderForAuth || - (harnessProviderForAuth === CODEX_HARNESS_AUTH_PROVIDER && - authProfileProviderForAuth === OPENAI_PROVIDER && - params.authProfileMode === "api_key")); - const openAICanForwardCodexProfile = shouldRouteOpenAIThroughCodexAuthProvider({ - provider: providerForAuth, - harnessRuntime: params.harnessRuntime, - agentHarnessId: params.harnessId, - authProfileProvider: authProfileProviderForAuth, - authProfileId: params.sessionAuthProfileId, - config: params.config, - workspaceDir: params.workspaceDir, - }); + harnessProviderForAuth === authProfileProviderForAuth; const providerCanForwardProfile = !harnessProviderForAuth && providerForAuth === authProfileProviderForAuth; - const canForwardProfile = - providerCanForwardProfile || harnessCanForwardProfile || openAICanForwardCodexProfile; + const canForwardProfile = providerCanForwardProfile || harnessCanForwardProfile; return { providerForAuth, diff --git a/src/agents/runtime-plan/build.test.ts b/src/agents/runtime-plan/build.test.ts index 3344e71838c3..ed6fba5af79f 100644 --- a/src/agents/runtime-plan/build.test.ts +++ b/src/agents/runtime-plan/build.test.ts @@ -133,8 +133,8 @@ describe("AgentRuntimePlan", () => { }); expect(plan.auth.providerForAuth).toBe("openai"); - expect(plan.auth.authProfileProviderForAuth).toBe("openai-codex"); - expect(plan.auth.harnessAuthProvider).toBe("openai-codex"); + expect(plan.auth.authProfileProviderForAuth).toBe("openai"); + expect(plan.auth.harnessAuthProvider).toBe("openai"); expect(plan.auth.forwardedAuthProfileId).toBe("openai-codex:work"); expect(plan.delivery.isSilentPayload({ text: "NO_REPLY\n\nNO_REPLY" })).toBe(true); expect(plan.delivery.isSilentPayload({ text: '{"action":"NO_REPLY"}' })).toBe(true); @@ -219,7 +219,7 @@ describe("AgentRuntimePlan", () => { expect(plan.auth.providerForAuth).toBe("openai"); expect(plan.auth.authProfileProviderForAuth).toBe("openai"); - expect(plan.auth.harnessAuthProvider).toBe("openai-codex"); + expect(plan.auth.harnessAuthProvider).toBe("openai"); expect(plan.auth.forwardedAuthProfileId).toBe("openai:work"); }); @@ -245,7 +245,7 @@ describe("AgentRuntimePlan", () => { ]); }); - it("does not forward non-api-key OpenAI profiles into the Codex harness auth slot", () => { + it("forwards OpenAI OAuth profiles into the Codex harness auth slot", () => { const plan = buildAgentRuntimePlan({ provider: "openai", modelId: "gpt-5.4", @@ -259,7 +259,7 @@ describe("AgentRuntimePlan", () => { workspaceDir: "/tmp/openclaw-runtime-plan", }); - expect(plan.auth.forwardedAuthProfileId).toBeUndefined(); + expect(plan.auth.forwardedAuthProfileId).toBe("openai:work"); }); it("forwards OpenAI Codex profiles for explicit OpenAI OpenClaw runs", () => { @@ -276,7 +276,7 @@ describe("AgentRuntimePlan", () => { }); expect(plan.auth.providerForAuth).toBe("openai"); - expect(plan.auth.authProfileProviderForAuth).toBe("openai-codex"); + expect(plan.auth.authProfileProviderForAuth).toBe("openai"); expect(plan.auth.forwardedAuthProfileId).toBe("openai-codex:work"); }); diff --git a/src/agents/simple-completion-runtime.selection.test.ts b/src/agents/simple-completion-runtime.selection.test.ts index a3c2bedf66c0..f1a7611ecc3a 100644 --- a/src/agents/simple-completion-runtime.selection.test.ts +++ b/src/agents/simple-completion-runtime.selection.test.ts @@ -74,7 +74,7 @@ describe("resolveSimpleCompletionSelectionForAgent", () => { expect(selection.profileId).toBe("work"); }); - it("uses Codex execution provider for OpenAI model refs with Codex runtime policy", () => { + it("keeps OpenAI as execution provider for OpenAI model refs with Codex runtime policy", () => { const cfg = { agents: { defaults: { @@ -91,7 +91,7 @@ describe("resolveSimpleCompletionSelectionForAgent", () => { ); expect(selection.provider).toBe("openai"); expect(selection.modelId).toBe("gpt-5.4-mini"); - expect(selection.runtimeProvider).toBe("openai-codex"); + expect(selection.runtimeProvider).toBe("openai"); }); it("falls back to runtime default model when no explicit model is configured", () => { diff --git a/src/agents/simple-completion-runtime.test.ts b/src/agents/simple-completion-runtime.test.ts index 6e2a2bcf1239..83dbea589b3b 100644 --- a/src/agents/simple-completion-runtime.test.ts +++ b/src/agents/simple-completion-runtime.test.ts @@ -520,7 +520,7 @@ describe("prepareSimpleCompletionModelForAgent", () => { } as OpenClawConfig; hoisted.resolveModelAsyncMock.mockResolvedValueOnce({ model: { - provider: "openai-codex", + provider: "openai", id: "gpt-5.4-mini", }, authStorage: { @@ -539,9 +539,9 @@ describe("prepareSimpleCompletionModelForAgent", () => { expectPreparedModelResult(result); expect(result.selection.provider).toBe("openai"); expect(result.selection.modelId).toBe("gpt-5.4-mini"); - expect(result.selection.runtimeProvider).toBe("openai-codex"); + expect(result.selection.runtimeProvider).toBe("openai"); expect(hoisted.resolveModelAsyncMock).toHaveBeenCalledWith( - "openai-codex", + "openai", "gpt-5.4-mini", expect.any(String), cfg, @@ -551,7 +551,7 @@ describe("prepareSimpleCompletionModelForAgent", () => { ); expect( (callArg(hoisted.getApiKeyForModelMock) as { model?: { provider?: string } }).model?.provider, - ).toBe("openai-codex"); + ).toBe("openai"); }); }); diff --git a/src/agents/simple-completion-runtime.ts b/src/agents/simple-completion-runtime.ts index ad0d600b3c14..1a07a43f87f6 100644 --- a/src/agents/simple-completion-runtime.ts +++ b/src/agents/simple-completion-runtime.ts @@ -24,7 +24,7 @@ import { resolveDefaultModelForAgent, resolveModelRefFromString, } from "./model-selection.js"; -import { OPENAI_CODEX_PROVIDER_ID, isOpenAIProvider } from "./openai-codex-routing.js"; +import { OPENAI_PROVIDER_ID, isOpenAIProvider } from "./openai-codex-routing.js"; import { prepareModelForSimpleCompletion } from "./simple-completion-transport.js"; type SimpleCompletionAuthStorage = { @@ -133,7 +133,7 @@ function resolveSimpleCompletionRuntimeProvider(params: { config: params.cfg, agentId: params.agentId, }); - return policy.runtime === "codex" ? { runtimeProvider: OPENAI_CODEX_PROVIDER_ID } : {}; + return policy.runtime === "codex" ? { runtimeProvider: OPENAI_PROVIDER_ID } : {}; } async function setRuntimeApiKeyForCompletion(params: { @@ -211,9 +211,14 @@ export async function prepareSimpleCompletionModel(params: { ? { allowBundledStaticCatalogFallback: params.allowBundledStaticCatalogFallback } : {}), skipAgentDiscovery: true, + authProfileId: params.profileId, + preferredProfile: params.preferredProfile, }, ) - : resolveModel(params.provider, params.modelId, params.agentDir, params.cfg); + : resolveModel(params.provider, params.modelId, params.agentDir, params.cfg, { + authProfileId: params.profileId, + preferredProfile: params.preferredProfile, + }); if (!resolved.model) { return { error: resolved.error ?? `Unknown model: ${params.provider}/${params.modelId}`, diff --git a/src/agents/simple-completion-transport.test.ts b/src/agents/simple-completion-transport.test.ts index b61ded18dc85..282ac89340ba 100644 --- a/src/agents/simple-completion-transport.test.ts +++ b/src/agents/simple-completion-transport.test.ts @@ -185,13 +185,13 @@ describe("prepareModelForSimpleCompletion", () => { "https://proxy.example.test/openai/codex", ], ])( - "uses OpenClaw transport for OpenAI Codex simple completions with baseUrl %s", + "uses OpenClaw transport for OpenAI Codex-response simple completions with baseUrl %s", (baseUrl, expectedBaseUrl) => { const model: Model<"openai-codex-responses"> = { id: "gpt-5.5", name: "GPT-5.5", api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", baseUrl, reasoning: true, input: ["text"], diff --git a/src/agents/simple-completion-transport.ts b/src/agents/simple-completion-transport.ts index 08b1880a7234..8fc154e1d2e2 100644 --- a/src/agents/simple-completion-transport.ts +++ b/src/agents/simple-completion-transport.ts @@ -52,7 +52,7 @@ function prepareCodexSimpleTransportModel( model: Model, cfg?: OpenClawConfig, ): Model | undefined { - if (model.provider !== "openai-codex" || model.api !== "openai-codex-responses") { + if (model.provider !== "openai" || model.api !== "openai-codex-responses") { return undefined; } diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index d8c7e1a5d183..44aa12ad4b47 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -2100,11 +2100,16 @@ describe("image tool data URL support", () => { it("applies model image maxBytes to data URLs", async () => { await withTempAgentDir(async (agentDir) => { - installImageUnderstandingProviderStubs(); const model = { ...makeModelDefinition("tiny-vision", ["text", "image"]), mediaInput: { image: { maxBytes: 1 } }, } satisfies ModelDefinitionConfig; + installImageUnderstandingProviderDeps([], { + resolveImageCompressionPolicy: async () => ({ + imageCount: 1, + models: [model.mediaInput.image], + }), + }); const cfg: OpenClawConfig = { agents: { defaults: { @@ -2135,21 +2140,31 @@ describe("image tool data URL support", () => { it("downscales data URL images to the resolved model side limit", async () => { await withTempAgentDir(async (agentDir) => { let observedDimensions: { width: number; height: number } | undefined; - installImageUnderstandingProviderStubs({ - id: "openai", - capabilities: ["image"], - describeImage: async (params) => { - observedDimensions = - params.mime === "image/png" - ? readPngDimensions(params.buffer) - : readJpegDimensions(params.buffer); - return { text: "ok", model: params.model }; - }, - }); const model = { ...makeModelDefinition("tiny-vision", ["text", "image"]), mediaInput: { image: { maxSidePx: 512, preferredSidePx: 512 } }, } satisfies ModelDefinitionConfig; + installImageUnderstandingProviderDeps( + [ + { + id: "openai", + capabilities: ["image"], + describeImage: async (params) => { + observedDimensions = + params.mime === "image/png" + ? readPngDimensions(params.buffer) + : readJpegDimensions(params.buffer); + return { text: "ok", model: params.model }; + }, + }, + ], + { + resolveImageCompressionPolicy: async () => ({ + imageCount: 1, + models: [model.mediaInput.image], + }), + }, + ); const cfg: OpenClawConfig = { agents: { defaults: { diff --git a/src/agents/tools/pdf-tool.model-config.test.ts b/src/agents/tools/pdf-tool.model-config.test.ts index 4fc2273d77b8..6bea72566dd6 100644 --- a/src/agents/tools/pdf-tool.model-config.test.ts +++ b/src/agents/tools/pdf-tool.model-config.test.ts @@ -174,7 +174,7 @@ describe("resolvePdfModelConfigForTool", () => { } as OpenClawConfig; expect(resolvePdfModelConfigForTool({ cfg, agentDir: TEST_AGENT_DIR })).toEqual({ - primary: "openai/gpt-5.4-mini", + primary: "openai/gpt-5.5", fallbacks: ["minimax/MiniMax-M2.7", "minimax-portal/MiniMax-M2.7"], }); }); diff --git a/src/agents/tools/video-generate-tool.test.ts b/src/agents/tools/video-generate-tool.test.ts index b9f770c3c253..42b86f73f101 100644 --- a/src/agents/tools/video-generate-tool.test.ts +++ b/src/agents/tools/video-generate-tool.test.ts @@ -345,15 +345,15 @@ describe("createVideoGenerateTool", () => { expect(emptyConfigTool).toBeNull(); }); - it("does not treat model aliases as video-generation auth profiles", () => { + it("treats legacy OpenAI-Codex auth profiles as canonical OpenAI video auth", () => { vi.spyOn(videoGenerationRuntime, "listRuntimeVideoGenerationProviders").mockReturnValue([]); - expect( + expectVideoGenerateTool( createVideoGenerateTool({ config: asConfig({}), authProfileStore: createAuthStore(["openai-codex"]), }), - ).toBeNull(); + ); }); it("registers when video-generation config is present", () => { diff --git a/src/agents/tools/video-generate-tool.ts b/src/agents/tools/video-generate-tool.ts index 7f0354c2aeb1..660a5d0bf179 100644 --- a/src/agents/tools/video-generate-tool.ts +++ b/src/agents/tools/video-generate-tool.ts @@ -40,6 +40,7 @@ import { recordRecentMediaGenerationTaskStartForSession, } from "../media-generation-task-status-shared.js"; import { getCustomProviderApiKey } from "../model-auth.js"; +import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import { ToolInputError, readNumberParam, readStringParam } from "./common.js"; import { decodeDataUrl } from "./image-tool.helpers.js"; import { @@ -240,12 +241,21 @@ function hasExplicitVideoGenerationModelConfig(cfg?: OpenClawConfig): boolean { return hasToolModelConfig(coerceToolModelConfig(cfg?.agents?.defaults?.videoGenerationModel)); } -function collectVideoGenerationModelProviderIds(modelConfig: ToolModelConfig): Set { +function collectVideoGenerationModelProviderIds(params: { + cfg: OpenClawConfig; + modelConfig: ToolModelConfig; + workspaceDir?: string; +}): Set { const providerIds = new Set(); - for (const modelRef of [modelConfig.primary, ...(modelConfig.fallbacks ?? [])]) { + for (const modelRef of [params.modelConfig.primary, ...(params.modelConfig.fallbacks ?? [])]) { const parsed = parseVideoGenerationModelRef(modelRef); if (parsed?.provider) { - providerIds.add(parsed.provider); + providerIds.add( + resolveProviderIdForAuth(parsed.provider, { + config: params.cfg, + ...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + }), + ); } } return providerIds; @@ -287,9 +297,11 @@ function shouldExposeVideoReferenceAudioParams(params: { }); const knownProviderIds = new Set(); const audioCandidateProviderIds = new Set(); - const explicitProviderIds = collectVideoGenerationModelProviderIds( - coerceToolModelConfig(params.cfg.agents?.defaults?.videoGenerationModel), - ); + const explicitProviderIds = collectVideoGenerationModelProviderIds({ + cfg: params.cfg, + modelConfig: coerceToolModelConfig(params.cfg.agents?.defaults?.videoGenerationModel), + ...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + }); for (const plugin of snapshot.plugins) { if ( diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index 790545e4fb73..8735e3a2c799 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -3158,7 +3158,7 @@ describe("runAgentTurnWithFallback", () => { state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {} }); state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => { const failedResult = await params.run("openai-codex", "gpt-5.4"); - expect(sessionEntry.providerOverride).toBe("openai-codex"); + expect(sessionEntry.providerOverride).toBe("openai"); expect(sessionEntry.modelOverride).toBe("gpt-5.4"); const classification = await params.classifyResult?.({ result: failedResult as { payloads?: [] }, @@ -5524,7 +5524,7 @@ describe("runAgentTurnWithFallback", () => { expect(result.kind).toBe("final"); if (result.kind === "final") { expect(result.payload.text).toBe( - "⚠️ Model login expired on the gateway for openai-codex. Re-auth with `openclaw models auth login --provider openai-codex`, then try again.", + "⚠️ Model login expired on the gateway for openai. Re-auth with `openclaw models auth login --provider openai`, then try again.", ); } }); @@ -5579,7 +5579,7 @@ describe("runAgentTurnWithFallback", () => { 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`.", + "⚠️ 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` or run `openclaw configure`.", ); } }); @@ -6125,12 +6125,12 @@ describe("runAgentTurnWithFallback", () => { expect(result.kind).toBe("success"); expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded run params", { - provider: "openai-codex", + provider: "openai", model: "gpt-5.4", authProfileId: undefined, authProfileIdSource: undefined, }); - expect(sessionEntry.providerOverride).toBe("openai-codex"); + expect(sessionEntry.providerOverride).toBe("openai"); expect(sessionEntry.modelOverride).toBe("gpt-5.4"); expect(sessionEntry.modelOverrideSource).toBe("auto"); expect(sessionEntry.authProfileOverride).toBeUndefined(); @@ -6262,7 +6262,7 @@ describe("runAgentTurnWithFallback", () => { }); expect(result.kind).toBe("success"); - expect(sessionEntry.providerOverride).toBe("openai-codex"); + expect(sessionEntry.providerOverride).toBe("openai"); expect(sessionEntry.modelOverride).toBe("gpt-5.4"); expect(sessionEntry.modelOverrideSource).toBe("auto"); expect(sessionEntry.modelOverrideFallbackOriginProvider).toBe("minimax"); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 187d76b78d6e..9cdcf90cf27c 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -721,7 +721,7 @@ function buildMissingApiKeyFailureText(message: string): string | null { 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`."; + 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` 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.`; @@ -1676,10 +1676,15 @@ export async function runAgentTurnWithFallback(params: { model: effectiveRun.model, } : candidateRun; + const persistedProvider = resolveOpenAIRuntimeProvider({ + provider, + config: runtimeConfig, + workspaceDir: params.followupRun.run.workspaceDir, + }); const applied = applyFallbackCandidateSelectionToEntry({ entry: activeSessionEntry, run: selectionRun, - provider, + provider: persistedProvider, model, force: candidateRun !== effectiveRun && Boolean(effectiveRun.autoFallbackPrimaryProbe), ...(effectiveRun.autoFallbackPrimaryProbe diff --git a/src/auto-reply/reply/commands-models.test.ts b/src/auto-reply/reply/commands-models.test.ts index 6917317edb80..41816230fb86 100644 --- a/src/auto-reply/reply/commands-models.test.ts +++ b/src/auto-reply/reply/commands-models.test.ts @@ -803,7 +803,7 @@ describe("handleModelsCommand", () => { expect(authLabelParams.workspaceDir).toBe("/tmp"); }); - it("labels OpenAI provider pages with the effective Codex auth provider set", async () => { + it("labels OpenAI provider pages with canonical and legacy auth provider ids", async () => { modelAuthLabelMocks.resolveModelAuthLabel.mockReturnValue( "oauth (openai-codex:user@example.com)", ); @@ -827,7 +827,7 @@ describe("handleModelsCommand", () => { ); expect(openaiAuthCall?.[0]).toMatchObject({ provider: "openai", - acceptedProviderIds: ["openai-codex"], + acceptedProviderIds: ["openai-codex", "openai"], }); }); diff --git a/src/commands/auth-choice-legacy.test.ts b/src/commands/auth-choice-legacy.test.ts index 886b679aacb9..2142e31f08b5 100644 --- a/src/commands/auth-choice-legacy.test.ts +++ b/src/commands/auth-choice-legacy.test.ts @@ -11,11 +11,11 @@ const manifestAuthChoices = vi.hoisted(() => [ }, { pluginId: "openai", - providerId: "openai-codex", - methodId: "cli", - choiceId: "openai-codex-cli", - choiceLabel: "OpenAI Codex CLI", - deprecatedChoiceIds: ["codex-cli"], + providerId: "openai", + methodId: "oauth", + choiceId: "openai", + choiceLabel: "ChatGPT Login", + deprecatedChoiceIds: ["openai-codex", "codex-cli"], }, ]); @@ -58,6 +58,13 @@ describe("auth choice legacy aliases", () => { expect(resolveLegacyAuthChoiceAliasesForCli({ env: authChoiceManifestEnv() })).toEqual([ "claude-cli", "codex-cli", + "openai-codex", ]); }); + + it("maps the old OpenAI Codex setup choice to OpenAI login", () => { + expect(normalizeLegacyOnboardAuthChoice("openai-codex", { env: authChoiceManifestEnv() })).toBe( + "openai", + ); + }); }); diff --git a/src/commands/auth-choice-legacy.ts b/src/commands/auth-choice-legacy.ts index b592da3c8d47..f073ad5b73aa 100644 --- a/src/commands/auth-choice-legacy.ts +++ b/src/commands/auth-choice-legacy.ts @@ -5,6 +5,13 @@ import { } from "../plugins/provider-auth-choices.js"; import type { AuthChoice } from "./onboard-types.js"; +const LEGACY_REPLACEMENT_AUTH_CHOICES = new Set([ + "claude-cli", + "codex-cli", + "openai-codex", + "openai-codex-device-code", +]); + function resolveLegacyCliBackendChoice( choice: string, params?: { @@ -13,7 +20,7 @@ function resolveLegacyCliBackendChoice( env?: NodeJS.ProcessEnv; }, ) { - if (!choice.endsWith("-cli")) { + if (!LEGACY_REPLACEMENT_AUTH_CHOICES.has(choice)) { return undefined; } return resolveManifestDeprecatedProviderAuthChoice(choice, params); @@ -30,7 +37,7 @@ export function resolveLegacyAuthChoiceAliasesForCli(params?: { }): ReadonlyArray { const manifestCliAliases = resolveManifestProviderAuthChoices(params) .flatMap((choice) => choice.deprecatedChoiceIds ?? []) - .filter((choice): choice is AuthChoice => choice.endsWith("-cli")) + .filter((choice): choice is AuthChoice => LEGACY_REPLACEMENT_AUTH_CHOICES.has(choice)) .toSorted((left, right) => left.localeCompare(right)); return manifestCliAliases; } diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index f31d1acddaf1..c8dcaa979d5b 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -10,7 +10,7 @@ async function normalizeLegacyChoice( if (authChoice === "oauth") { return "setup-token"; } - if (typeof authChoice !== "string" || !authChoice.endsWith("-cli")) { + if (typeof authChoice !== "string") { return authChoice; } const { normalizeLegacyOnboardAuthChoice } = await import("./auth-choice-legacy.js"); diff --git a/src/commands/auth-choice.model-check.test.ts b/src/commands/auth-choice.model-check.test.ts index 95ecfa9e9716..2926e7708b20 100644 --- a/src/commands/auth-choice.model-check.test.ts +++ b/src/commands/auth-choice.model-check.test.ts @@ -46,13 +46,14 @@ describe("warnIfModelConfigLooksOff", () => { expect(loadModelCatalog).not.toHaveBeenCalled(); expect(ensureAuthProfileStore).toHaveBeenCalledOnce(); - expect(listProfilesForProvider).toHaveBeenCalledOnce(); + expect(listProfilesForProvider).toHaveBeenCalledTimes(2); expect(listProfilesForProvider).toHaveBeenCalledWith( { version: 1, profiles: {} }, "openai-codex", ); + expect(listProfilesForProvider).toHaveBeenCalledWith({ version: 1, profiles: {} }, "openai"); expect(note).toHaveBeenCalledWith( - 'No auth configured for provider "openai-codex". The agent may fail until credentials are added. Run `openclaw models auth login --provider openai-codex`, `openclaw configure`, or set an API key env var.', + 'No auth configured for provider "openai-codex". The agent may fail until credentials are added. Run `openclaw models auth login --provider openai`, `openclaw configure`, or set an API key env var.', "Model check", ); }); diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 51531a4244d4..6c3e897c410e 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -511,6 +511,7 @@ async function createDefaultProviderPlugins(): Promise { flagName: "--openai-api-key", envVar: "OPENAI_API_KEY", promptMessage: "Enter OpenAI API key", + profileId: "openai:api-key", defaultModel: "openai/gpt-5.5", }), await createApiKeyProvider({ @@ -1181,11 +1182,11 @@ describe("applyAuthChoice", () => { expect(providerResolveInput.mode).toBe("setup"); expectPromptMessageContaining(confirm, "OPENAI_API_KEY"); expect(text).not.toHaveBeenCalled(); - expectAuthProfileConfig(result, "openai:default", { + expectAuthProfileConfig(result, "openai:api-key", { provider: "openai", mode: "api_key", }); - expect((await readAuthProfile("openai:default"))?.key).toBe("sk-openai-explicit"); + expect((await readAuthProfile("openai:api-key"))?.key).toBe("sk-openai-explicit"); }); it("keeps existing default model for explicit provider keys when setDefaultModel=false", async () => { diff --git a/src/commands/doctor-auth-flat-profiles.test.ts b/src/commands/doctor-auth-flat-profiles.test.ts index e072e590ca3a..0bd3907c51c6 100644 --- a/src/commands/doctor-auth-flat-profiles.test.ts +++ b/src/commands/doctor-auth-flat-profiles.test.ts @@ -1,11 +1,17 @@ import fs from "node:fs"; import { afterEach, describe, expect, it, vi } from "vitest"; import { clearRuntimeAuthProfileStoreSnapshots } from "../agents/auth-profiles/store.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createOpenClawTestState, type OpenClawTestState, } from "../test-utils/openclaw-test-state.js"; -import { maybeRepairLegacyFlatAuthProfileStores } from "./doctor-auth-flat-profiles.js"; +import { + collectOpenAICodexAuthProfileStoreIdMap, + maybeRepairLegacyFlatAuthProfileStores, + maybeRepairOpenAICodexAuthConfig, + maybeRepairOpenAICodexAuthProfileStores, +} from "./doctor-auth-flat-profiles.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; const states: OpenClawTestState[] = []; @@ -158,3 +164,410 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => { ); }); }); + +describe("maybeRepairOpenAICodexAuthConfig", () => { + it("renames legacy OpenAI Codex config profiles and merges auth order", () => { + const cfg = { + auth: { + profiles: { + "openai:default": { + provider: "openai", + mode: "api_key", + }, + "openai-codex:default": { + provider: "openai-codex", + mode: "oauth", + email: "chatgpt@example.com", + }, + }, + order: { + openai: ["openai:default"], + "openai-codex": ["openai-codex:default"], + }, + }, + agents: { + defaults: { + models: { + "openai/gpt-5.5": { + agentRuntime: { + id: "codex", + authProfileId: "openai-codex:default", + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairOpenAICodexAuthConfig(cfg); + const migrated = result.config as OpenClawConfig & { + agents?: { + defaults?: { + models?: Record; + }; + }; + }; + + expect(result.changes).toStrictEqual([ + "Migrated legacy OpenAI Codex auth profile config to the canonical OpenAI provider.", + ]); + expect(result.config.auth?.profiles).toEqual({ + "openai:default": { + provider: "openai", + mode: "api_key", + }, + "openai:chatgpt-default": { + provider: "openai", + mode: "oauth", + email: "chatgpt@example.com", + }, + }); + expect(result.config.auth?.order).toEqual({ + openai: ["openai:chatgpt-default", "openai:default"], + }); + expect(migrated.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.authProfileId).toBe( + "openai:chatgpt-default", + ); + }); + + it("canonicalizes legacy OpenAI Codex auth order entries without config profiles", () => { + const cfg = { + auth: { + order: { + "openai-codex": ["openai-codex:work"], + }, + }, + agents: { + defaults: { + models: { + "openai/gpt-5.5": { + agentRuntime: { + authProfileId: "openai-codex:work", + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairOpenAICodexAuthConfig(cfg); + const migrated = result.config as OpenClawConfig & { + agents?: { + defaults?: { + models?: Record; + }; + }; + }; + + expect(result.changes).toStrictEqual([ + "Migrated legacy OpenAI Codex auth profile config to the canonical OpenAI provider.", + ]); + expect(result.config.auth?.order).toEqual({ + openai: ["openai:work"], + }); + expect(migrated.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.authProfileId).toBe( + "openai:work", + ); + }); + + it("uses auth-store profile renames when canonicalizing config-only auth order", () => { + const cfg = { + auth: { + order: { + "openai-codex": ["openai-codex:default"], + }, + }, + agents: { + defaults: { + models: { + "openai/gpt-5.5": { + agentRuntime: { + authProfileId: "openai-codex:default", + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairOpenAICodexAuthConfig(cfg, { + profileIdMap: new Map([["openai-codex:default", "openai:chatgpt-default"]]), + }); + const migrated = result.config as OpenClawConfig & { + agents?: { + defaults?: { + models?: Record; + }; + }; + }; + + expect(result.config.auth?.order).toEqual({ + openai: ["openai:chatgpt-default"], + }); + expect(migrated.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.authProfileId).toBe( + "openai:chatgpt-default", + ); + }); + + it("uses auth-store profile renames for profile refs when config has no auth block", () => { + const cfg = { + agents: { + defaults: { + models: { + "openai/gpt-5.5": { + agentRuntime: { + authProfileId: "openai-codex:default", + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairOpenAICodexAuthConfig(cfg, { + profileIdMap: new Map([["openai-codex:default", "openai:chatgpt-default"]]), + }); + const migrated = result.config as OpenClawConfig & { + agents?: { + defaults?: { + models?: Record; + }; + }; + }; + + expect(result.changes).toStrictEqual([ + "Migrated legacy OpenAI Codex auth profile config to the canonical OpenAI provider.", + ]); + expect(migrated.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.authProfileId).toBe( + "openai:chatgpt-default", + ); + expect(result.config.auth).toBeUndefined(); + }); + + it("does not rewrite unrelated config strings that look like profile ids", () => { + const cfg = { + auth: { + order: { + "openai-codex": ["openai-codex:default"], + }, + }, + agents: { + defaults: { + systemPrompt: "Use openai-codex:default as literal text.", + models: { + "openai/gpt-5.5": { + agentRuntime: { + authProfileId: "openai-codex:default", + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairOpenAICodexAuthConfig(cfg, { + profileIdMap: new Map([["openai-codex:default", "openai:chatgpt-default"]]), + }); + const migrated = result.config as OpenClawConfig & { + agents?: { + defaults?: { + systemPrompt?: string; + models?: Record; + }; + }; + }; + + expect(migrated.agents?.defaults?.systemPrompt).toBe( + "Use openai-codex:default as literal text.", + ); + expect(migrated.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.authProfileId).toBe( + "openai:chatgpt-default", + ); + }); + + it("uses auth-store profile renames when canonicalizing config profile entries", () => { + const cfg = { + auth: { + profiles: { + "openai-codex:default": { + provider: "openai-codex", + mode: "oauth", + }, + }, + order: { + "openai-codex": ["openai-codex:default"], + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairOpenAICodexAuthConfig(cfg, { + profileIdMap: new Map([["openai-codex:default", "openai:chatgpt-default"]]), + }); + + expect(result.config.auth?.profiles).toEqual({ + "openai:chatgpt-default": { + provider: "openai", + mode: "oauth", + }, + }); + expect(result.config.auth?.order).toEqual({ + openai: ["openai:chatgpt-default"], + }); + }); + + it("keeps existing OpenAI config profiles when auth-store renames collide", () => { + const cfg = { + auth: { + profiles: { + "openai:default": { + provider: "openai", + mode: "api_key", + }, + "openai-codex:default": { + provider: "openai-codex", + mode: "oauth", + email: "chatgpt@example.com", + }, + }, + order: { + openai: ["openai:default"], + "openai-codex": ["openai-codex:default"], + }, + }, + agents: { + defaults: { + models: { + "openai/gpt-5.5": { + agentRuntime: { + authProfileId: "openai-codex:default", + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairOpenAICodexAuthConfig(cfg, { + profileIdMap: new Map([["openai-codex:default", "openai:default"]]), + }); + const migrated = result.config as OpenClawConfig & { + agents?: { + defaults?: { + models?: Record; + }; + }; + }; + + expect(result.config.auth?.profiles).toEqual({ + "openai:default": { + provider: "openai", + mode: "api_key", + }, + "openai:chatgpt-default": { + provider: "openai", + mode: "oauth", + email: "chatgpt@example.com", + }, + }); + expect(result.config.auth?.order).toEqual({ + openai: ["openai:chatgpt-default", "openai:default"], + }); + expect(migrated.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.authProfileId).toBe( + "openai:chatgpt-default", + ); + }); +}); + +describe("maybeRepairOpenAICodexAuthProfileStores", () => { + it("collects the store-derived legacy OpenAI Codex profile id map", async () => { + const state = await makeTestState(); + await state.writeAuthProfiles({ + version: 1, + profiles: { + "openai:default": { + type: "api_key", + provider: "openai", + key: "sk-openai", + }, + "openai-codex:default": { + type: "oauth", + provider: "openai-codex", + access: "access", + refresh: "refresh", + expires: 9999999999999, + }, + }, + }); + + expect( + Array.from( + collectOpenAICodexAuthProfileStoreIdMap({ + cfg: {}, + env: state.env, + }), + ), + ).toEqual([["openai-codex:default", "openai:chatgpt-default"]]); + }); + + it("renames legacy OpenAI Codex auth store profiles with a backup", async () => { + const state = await makeTestState(); + const legacy = { + version: 1, + profiles: { + "openai-codex:work": { + type: "oauth", + provider: "openai-codex", + access: "access", + refresh: "refresh", + expires: 9999999999999, + }, + }, + order: { + "openai-codex": ["openai-codex:work"], + }, + usageStats: { + "openai-codex:work": { + blockedUntil: 9999999999999, + blockedReason: "subscription_limit", + }, + }, + }; + const authPath = await state.writeAuthProfiles(legacy); + + const result = await maybeRepairOpenAICodexAuthProfileStores({ + cfg: {}, + env: state.env, + now: () => 789, + }); + + expect(result.detected).toEqual([authPath]); + expect(result.changes).toStrictEqual([ + `Migrated 1 OpenAI Codex auth profile(s) in ${authPath} to provider "openai" (backup: ${authPath}.openai-provider-unification.789.bak).`, + ]); + expect(result.warnings).toStrictEqual([]); + expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({ + version: 1, + profiles: { + "openai:work": { + type: "oauth", + provider: "openai", + access: "access", + refresh: "refresh", + expires: 9999999999999, + }, + }, + order: { + openai: ["openai:work"], + }, + usageStats: { + "openai:work": { + blockedUntil: 9999999999999, + blockedReason: "subscription_limit", + }, + }, + }); + expect( + JSON.parse(fs.readFileSync(`${authPath}.openai-provider-unification.789.bak`, "utf8")), + ).toEqual(legacy); + }); +}); diff --git a/src/commands/doctor-auth-flat-profiles.ts b/src/commands/doctor-auth-flat-profiles.ts index 3aa74c95ef26..ca4b6ff65375 100644 --- a/src/commands/doctor-auth-flat-profiles.ts +++ b/src/commands/doctor-auth-flat-profiles.ts @@ -521,3 +521,364 @@ export async function maybeRepairCanonicalApiKeyFieldAlias(params: { } return result; } + +const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const OPENAI_PROVIDER_ID = "openai"; + +function isLegacyOpenAICodexProvider(value: unknown): boolean { + return ( + typeof value === "string" && value.trim().toLowerCase() === LEGACY_OPENAI_CODEX_PROVIDER_ID + ); +} + +function isLegacyOpenAICodexProfileId(profileId: string): boolean { + return profileId.trim().toLowerCase().startsWith(`${LEGACY_OPENAI_CODEX_PROVIDER_ID}:`); +} + +function canonicalOpenAIProfileSuffix(profileId: string): string { + return profileId.slice(profileId.indexOf(":") + 1).trim() || "default"; +} + +function allocateOpenAIProfileId(legacyProfileId: string, occupied: Set): string { + const suffix = canonicalOpenAIProfileSuffix(legacyProfileId); + const direct = `${OPENAI_PROVIDER_ID}:${suffix}`; + if (!occupied.has(direct)) { + occupied.add(direct); + return direct; + } + const chatgpt = `${OPENAI_PROVIDER_ID}:chatgpt-${suffix}`; + if (!occupied.has(chatgpt)) { + occupied.add(chatgpt); + return chatgpt; + } + for (let index = 2; ; index += 1) { + const candidate = `${chatgpt}-${index}`; + if (!occupied.has(candidate)) { + occupied.add(candidate); + return candidate; + } + } +} + +function canonicalizeOpenAIProfileEntries( + profiles: Record, + options?: { profileIdMap?: ReadonlyMap }, +): { + profileIdMap: Map; + changed: boolean; +} { + const occupied = new Set(Object.keys(profiles).filter((id) => !isLegacyOpenAICodexProfileId(id))); + const reservedMappedIds = new Set(options?.profileIdMap?.values() ?? []); + const profileIdMap = new Map(); + let changed = false; + + for (const [profileId, rawProfile] of Object.entries({ ...profiles })) { + if (!isRecord(rawProfile)) { + continue; + } + const legacyId = isLegacyOpenAICodexProfileId(profileId); + const legacyProvider = isLegacyOpenAICodexProvider(rawProfile.provider); + if (!legacyId && !legacyProvider) { + continue; + } + const mappedProfileId = legacyId ? options?.profileIdMap?.get(profileId) : undefined; + const nextProfileId = + mappedProfileId && !occupied.has(mappedProfileId) + ? mappedProfileId + : legacyId + ? allocateOpenAIProfileId(profileId, new Set([...occupied, ...reservedMappedIds])) + : profileId; + occupied.add(nextProfileId); + const nextProfile = { + ...rawProfile, + provider: OPENAI_PROVIDER_ID, + }; + if (nextProfileId !== profileId) { + delete profiles[profileId]; + profileIdMap.set(profileId, nextProfileId); + } + profiles[nextProfileId] = nextProfile; + changed = true; + } + + return { profileIdMap, changed }; +} + +function replaceMappedProfileId(value: unknown, profileIdMap: Map): unknown { + if (typeof value === "string") { + return profileIdMap.get(value) ?? value; + } + if (Array.isArray(value)) { + let changed = false; + const next = value.map((entry) => { + const replaced = replaceMappedProfileId(entry, profileIdMap); + changed ||= replaced !== entry; + return replaced; + }); + return changed ? next : value; + } + if (!isRecord(value)) { + return value; + } + let changed = false; + for (const [key, entry] of Object.entries(value)) { + const replaced = replaceMappedProfileId(entry, profileIdMap); + if (replaced !== entry) { + value[key] = replaced; + changed = true; + } + } + return changed ? value : value; +} + +const AUTH_PROFILE_REF_KEYS = new Set(["authProfileId"]); + +function rewriteMappedAuthProfileRefs( + value: unknown, + profileIdMap: ReadonlyMap, +): boolean { + if (Array.isArray(value)) { + return value.reduce( + (changed, entry) => rewriteMappedAuthProfileRefs(entry, profileIdMap) || changed, + false, + ); + } + if (!isRecord(value)) { + return false; + } + + let changed = false; + for (const [key, entry] of Object.entries(value)) { + if (AUTH_PROFILE_REF_KEYS.has(key) && typeof entry === "string") { + const replaced = profileIdMap.get(entry); + if (replaced && replaced !== entry) { + value[key] = replaced; + changed = true; + } + continue; + } + changed = rewriteMappedAuthProfileRefs(entry, profileIdMap) || changed; + } + return changed; +} + +function canonicalizeOpenAIAuthOrder( + auth: Record, + profileIdMap: Map, +): boolean { + if (!isRecord(auth.order)) { + return false; + } + const order = auth.order; + let changed = false; + const existingCanonicalOrder = Array.isArray(order[OPENAI_PROVIDER_ID]) + ? [...(order[OPENAI_PROVIDER_ID] as unknown[])] + : []; + const legacyOrder = Array.isArray(order[LEGACY_OPENAI_CODEX_PROVIDER_ID]) + ? (order[LEGACY_OPENAI_CODEX_PROVIDER_ID] as unknown[]) + : []; + const canonicalOrder = [...legacyOrder, ...existingCanonicalOrder]; + const occupiedProfileIds = new Set( + canonicalOrder.filter( + (entry): entry is string => typeof entry === "string" && !isLegacyOpenAICodexProfileId(entry), + ), + ); + for (const profileId of profileIdMap.values()) { + occupiedProfileIds.add(profileId); + } + + if (legacyOrder.length > 0) { + delete order[LEGACY_OPENAI_CODEX_PROVIDER_ID]; + changed = true; + } + + const rewritten = canonicalOrder + .map((entry) => { + if (typeof entry !== "string") { + return entry; + } + const mapped = profileIdMap.get(entry); + if (mapped) { + return mapped; + } + if (!isLegacyOpenAICodexProfileId(entry)) { + return entry; + } + const canonicalProfileId = allocateOpenAIProfileId(entry, occupiedProfileIds); + profileIdMap.set(entry, canonicalProfileId); + return canonicalProfileId; + }) + .filter( + (entry, index, entries) => typeof entry !== "string" || entries.indexOf(entry) === index, + ); + if (rewritten.length > 0) { + order[OPENAI_PROVIDER_ID] = rewritten; + } else if (OPENAI_PROVIDER_ID in order) { + delete order[OPENAI_PROVIDER_ID]; + } + return changed || rewritten.some((entry, index) => entry !== canonicalOrder[index]); +} + +function renameMappedProfileIdKeys( + record: Record, + profileIdMap: Map, +): boolean { + let changed = false; + for (const [key, value] of Object.entries({ ...record })) { + const nextKey = profileIdMap.get(key); + if (!nextKey || nextKey === key) { + continue; + } + delete record[key]; + record[nextKey] = value; + changed = true; + } + return changed; +} + +export function maybeRepairOpenAICodexAuthConfig( + cfg: OpenClawConfig, + options?: { profileIdMap?: ReadonlyMap }, +): { + config: OpenClawConfig; + changes: string[]; + warnings: string[]; +} { + const config = structuredClone(cfg); + const root = config as Record; + const auth = isRecord(root.auth) ? root.auth : undefined; + const profileIdMap = new Map(options?.profileIdMap); + let changed = false; + if (isRecord(auth?.profiles)) { + const rewrite = canonicalizeOpenAIProfileEntries(auth.profiles, { profileIdMap }); + for (const [from, to] of rewrite.profileIdMap) { + profileIdMap.set(from, to); + } + changed ||= rewrite.changed; + } + if (auth) { + const orderChanged = canonicalizeOpenAIAuthOrder(auth, profileIdMap); + changed ||= orderChanged; + } + if (profileIdMap.size > 0 && rewriteMappedAuthProfileRefs(config, profileIdMap)) { + changed = true; + } + if (!changed) { + return { config, changes: [], warnings: [] }; + } + return { + config, + changes: ["Migrated legacy OpenAI Codex auth profile config to the canonical OpenAI provider."], + warnings: [], + }; +} + +type OpenAICodexAuthStoreRepair = { + authPath: string; + raw: Record; + profileIdMap: Map; + changed: boolean; +}; + +function resolveOpenAICodexAuthStoreRepair( + candidate: AuthProfileRepairCandidate, + profileIdMap?: ReadonlyMap, +): OpenAICodexAuthStoreRepair | null { + if (!fs.existsSync(candidate.authPath)) { + return null; + } + const raw = loadJsonFile(candidate.authPath); + if (!isRecord(raw) || !isRecord(raw.profiles)) { + return null; + } + const rewrite = canonicalizeOpenAIProfileEntries(raw.profiles, { profileIdMap }); + const orderChanged = canonicalizeOpenAIAuthOrder(raw, rewrite.profileIdMap); + const usageChanged = isRecord(raw.usageStats) + ? renameMappedProfileIdKeys(raw.usageStats, rewrite.profileIdMap) + : false; + if (rewrite.profileIdMap.size > 0) { + replaceMappedProfileId(raw, rewrite.profileIdMap); + } + const changed = rewrite.changed || orderChanged || usageChanged; + return changed + ? { + authPath: candidate.authPath, + raw, + profileIdMap: rewrite.profileIdMap, + changed, + } + : null; +} + +export function collectOpenAICodexAuthProfileStoreIdMap(params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; +}): Map { + const env = params.env ?? process.env; + const occupiedProfileIds = new Set(); + const legacyProfileIds = new Set(); + const profileIdMap = new Map(); + for (const candidate of listAuthProfileRepairCandidates(params.cfg, env)) { + if (!fs.existsSync(candidate.authPath)) { + continue; + } + const raw = loadJsonFile(candidate.authPath); + if (!isRecord(raw) || !isRecord(raw.profiles)) { + continue; + } + for (const profileId of Object.keys(raw.profiles)) { + if (isLegacyOpenAICodexProfileId(profileId)) { + legacyProfileIds.add(profileId); + } else { + occupiedProfileIds.add(profileId); + } + } + } + for (const profileId of [...legacyProfileIds].toSorted((a, b) => a.localeCompare(b))) { + profileIdMap.set(profileId, allocateOpenAIProfileId(profileId, occupiedProfileIds)); + } + return profileIdMap; +} + +function backupOpenAIProviderUnification(authPath: string, now: () => number): string { + const backupPath = `${authPath}.openai-provider-unification.${now()}.bak`; + fs.copyFileSync(authPath, backupPath); + return backupPath; +} + +export async function maybeRepairOpenAICodexAuthProfileStores(params: { + cfg: OpenClawConfig; + now?: () => number; + env?: NodeJS.ProcessEnv; +}): Promise { + const now = params.now ?? Date.now; + const env = params.env ?? process.env; + const profileIdMap = collectOpenAICodexAuthProfileStoreIdMap({ cfg: params.cfg, env }); + const repairs = listAuthProfileRepairCandidates(params.cfg, env) + .map((candidate) => resolveOpenAICodexAuthStoreRepair(candidate, profileIdMap)) + .filter((entry): entry is OpenAICodexAuthStoreRepair => entry !== null); + const result: LegacyFlatAuthProfileRepairResult = { + detected: repairs.map((entry) => entry.authPath), + changes: [], + warnings: [], + }; + if (repairs.length === 0) { + return result; + } + for (const entry of repairs) { + try { + const backupPath = backupOpenAIProviderUnification(entry.authPath, now); + fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`); + const movedCount = entry.profileIdMap.size; + result.changes.push( + `Migrated ${movedCount} OpenAI Codex auth profile(s) in ${shortenHomePath(entry.authPath)} to provider "openai" (backup: ${shortenHomePath(backupPath)}).`, + ); + } catch (err) { + result.warnings.push( + `Failed to migrate OpenAI Codex auth profiles in ${shortenHomePath(entry.authPath)}: ${String(err)}`, + ); + } + } + clearRuntimeAuthProfileStoreSnapshots(); + return result; +} diff --git a/src/commands/doctor-auth.hints.test.ts b/src/commands/doctor-auth.hints.test.ts index 71c4429a4016..677e1e764cfa 100644 --- a/src/commands/doctor-auth.hints.test.ts +++ b/src/commands/doctor-auth.hints.test.ts @@ -35,7 +35,7 @@ describe("resolveUnusableProfileHint", () => { "OAuth token refresh failed for openai-codex: refresh_token_reused. Please try again or re-authenticate.", }), ).toBe( - "- openai-codex:default: re-auth required [refresh_token_reused] — Run `openclaw models auth login --provider openai-codex`.", + "- openai-codex:default: re-auth required [refresh_token_reused] — Run `openclaw models auth login --provider openai`.", ); }); @@ -48,7 +48,7 @@ describe("resolveUnusableProfileHint", () => { "OAuth token refresh failed for openai-codex: temporary upstream issue. Please try again or re-authenticate.", }), ).toBe( - "- openai-codex:default: OAuth refresh failed — Try again; if this persists, run `openclaw models auth login --provider openai-codex`.", + "- openai-codex:default: OAuth refresh failed — Try again; if this persists, run `openclaw models auth login --provider openai`.", ); }); @@ -61,7 +61,7 @@ describe("resolveUnusableProfileHint", () => { "OAuth token refresh failed for openai-codex`\nrm -rf /: invalid_grant. Please try again or re-authenticate.", }), ).toBe( - "- openai-codex:default: re-auth required [invalid_grant] — Run `openclaw models auth login --provider openai-codex`.", + "- openai-codex:default: re-auth required [invalid_grant] — Run `openclaw models auth login --provider openai`.", ); }); }); diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index bfaa94ec911e..b2b043a7923c 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -36,7 +36,7 @@ import { isRecord } from "../utils.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; import { buildProviderAuthRecoveryHint } from "./provider-auth-guidance.js"; -const CODEX_PROVIDER_ID = "openai-codex"; +const CODEX_PROVIDER_ID = "openai"; const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth"; const OPENAI_BASE_URL = "https://api.openai.com/v1"; const LEGACY_CODEX_APIS = new Set(["openai-responses", "openai-completions"]); diff --git a/src/commands/doctor/repair-sequencing.ts b/src/commands/doctor/repair-sequencing.ts index 28a9be4bb9ad..06b1e21b64ba 100644 --- a/src/commands/doctor/repair-sequencing.ts +++ b/src/commands/doctor/repair-sequencing.ts @@ -1,5 +1,10 @@ import { sanitizeForLog } from "../../../packages/terminal-core/src/ansi.js"; import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; +import { + collectOpenAICodexAuthProfileStoreIdMap, + maybeRepairOpenAICodexAuthConfig, + maybeRepairOpenAICodexAuthProfileStores, +} from "../doctor-auth-flat-profiles.js"; import { maybeRepairLegacyOAuthSidecarProfiles } from "../doctor-auth-oauth-sidecar.js"; import { maybeRepairManagedNpmOpenClawPeerLinks, @@ -92,6 +97,14 @@ export async function runDoctorRepairSequence(params: { changes: codexRouteRepair.changes, warnings: codexRouteRepair.warnings, }); + applyMutation( + maybeRepairOpenAICodexAuthConfig(state.candidate, { + profileIdMap: collectOpenAICodexAuthProfileStoreIdMap({ + cfg: state.candidate, + env, + }), + }), + ); applyMutation( await maybeRepairContextEngineHostCompatibility({ cfg: state.candidate, @@ -155,6 +168,16 @@ export async function runDoctorRepairSequence(params: { if (legacyOAuthSidecarRepair.warnings.length > 0) { warningNotes.push(sanitizeLines(legacyOAuthSidecarRepair.warnings)); } + const openAIAuthProviderRepair = await maybeRepairOpenAICodexAuthProfileStores({ + cfg: state.candidate, + env, + }); + if (openAIAuthProviderRepair.changes.length > 0) { + changeNotes.push(sanitizeLines(openAIAuthProviderRepair.changes)); + } + if (openAIAuthProviderRepair.warnings.length > 0) { + warningNotes.push(sanitizeLines(openAIAuthProviderRepair.warnings)); + } const staleOAuthShadowRepair = await repairStaleOAuthProfileShadows({ cfg: state.candidate, env, diff --git a/src/commands/doctor/shared/codex-route-warnings.test.ts b/src/commands/doctor/shared/codex-route-warnings.test.ts index e3a74eee3a79..eca6bf8dfe03 100644 --- a/src/commands/doctor/shared/codex-route-warnings.test.ts +++ b/src/commands/doctor/shared/codex-route-warnings.test.ts @@ -1862,6 +1862,13 @@ describe("collectCodexRouteWarnings", () => { model: "openai-codex/gpt-5.4-mini", }, }, + imageGenerationModel: { + primary: "openai-codex/gpt-image-2", + fallbacks: ["openai-codex/gpt-image-1"], + }, + videoGenerationModel: { + primary: "openai-codex/sora-2", + }, models: { "openai-codex/gpt-5.5": { alias: "codex" }, }, @@ -1907,6 +1914,9 @@ describe("collectCodexRouteWarnings", () => { "Repaired Codex model routes:", "- agents.defaults.model.primary: openai-codex/gpt-5.5 -> openai/gpt-5.5.", "- agents.defaults.model.fallbacks.0: openai-codex/gpt-5.4 -> openai/gpt-5.4.", + "- agents.defaults.imageGenerationModel.primary: openai-codex/gpt-image-2 -> openai/gpt-image-2.", + "- agents.defaults.imageGenerationModel.fallbacks.0: openai-codex/gpt-image-1 -> openai/gpt-image-1.", + "- agents.defaults.videoGenerationModel.primary: openai-codex/sora-2 -> openai/sora-2.", "- agents.defaults.heartbeat.model: openai-codex/gpt-5.4-mini -> openai/gpt-5.4-mini.", "- agents.defaults.subagents.model.primary: openai-codex/gpt-5.5 -> openai/gpt-5.5.", "- agents.defaults.subagents.model.fallbacks.0: openai-codex/gpt-5.4 -> openai/gpt-5.4.", @@ -1936,6 +1946,13 @@ describe("collectCodexRouteWarnings", () => { }); expect(result.cfg.agents?.defaults?.compaction?.model).toBeUndefined(); expect(result.cfg.agents?.defaults?.compaction?.memoryFlush?.model).toBe("openai/gpt-5.4-mini"); + expect(result.cfg.agents?.defaults?.imageGenerationModel).toEqual({ + primary: "openai/gpt-image-2", + fallbacks: ["openai/gpt-image-1"], + }); + expect(result.cfg.agents?.defaults?.videoGenerationModel).toEqual({ + primary: "openai/sora-2", + }); expect(result.cfg.agents?.defaults?.agentRuntime).toBeUndefined(); expect(result.cfg.agents?.defaults?.models).toEqual({ "openai/gpt-5.5": { alias: "codex", agentRuntime: { id: "codex" } }, diff --git a/src/commands/doctor/shared/codex-route-warnings.ts b/src/commands/doctor/shared/codex-route-warnings.ts index 098eddcd1698..fa6ade5ae2d0 100644 --- a/src/commands/doctor/shared/codex-route-warnings.ts +++ b/src/commands/doctor/shared/codex-route-warnings.ts @@ -47,6 +47,7 @@ type SharedDefaultCompactionOverrideConsumers = Record; const COMPACTION_OVERRIDE_KEYS: readonly CompactionOverrideKey[] = ["model", "provider"]; +const AGENT_MEDIA_MODEL_CONFIG_KEYS = ["imageGenerationModel", "videoGenerationModel"] as const; const LOSSLESS_CONTEXT_ENGINE_ID = "lossless-claw"; type SessionRouteRepairResult = { changed: boolean; @@ -868,6 +869,13 @@ function collectAgentModelRefs(params: { runtime: key === "model" ? params.runtime : undefined, }); } + for (const key of AGENT_MEDIA_MODEL_CONFIG_KEYS) { + collectModelConfigSlot({ + hits: params.hits, + path: `${params.path}.${key}`, + value: agent[key], + }); + } collectStringModelSlot({ hits: params.hits, path: `${params.path}.heartbeat.model`, @@ -1706,6 +1714,14 @@ function rewriteAgentModelRefs(params: { key: "model", path: `${params.path}.compaction.memoryFlush.model`, }); + for (const key of AGENT_MEDIA_MODEL_CONFIG_KEYS) { + rewriteModelConfigSlot({ + hits: params.hits, + container: agent, + key, + path: `${params.path}.${key}`, + }); + } if (params.rewriteModelsMap) { const start = params.hits.length; rewriteModelsMap({ diff --git a/src/commands/migrate.test.ts b/src/commands/migrate.test.ts index bd4d1f787bb9..ec3839b89806 100644 --- a/src/commands/migrate.test.ts +++ b/src/commands/migrate.test.ts @@ -108,7 +108,7 @@ function authPlan(status: MigrationPlan["items"][number]["status"] = "skipped"): }, items: [ { - id: "auth:openai-codex", + id: "auth:openai", kind: "auth", action: status === "planned" ? "create" : "skip", status, diff --git a/src/commands/models.auth.provider-resolution.test.ts b/src/commands/models.auth.provider-resolution.test.ts index ef8794564dff..f120526b6874 100644 --- a/src/commands/models.auth.provider-resolution.test.ts +++ b/src/commands/models.auth.provider-resolution.test.ts @@ -15,12 +15,14 @@ describe("resolveRequestedLoginProviderOrThrow", () => { it("returns null and resolves provider by id/alias", () => { const providers = [ makeProvider({ id: "google-gemini-cli", aliases: ["gemini-cli"] }), + makeProvider({ id: "openai", aliases: ["openai-codex"] }), makeProvider({ id: "minimax-portal" }), ]; const scenarios = [ { requested: undefined, expectedId: null }, { requested: "google-gemini-cli", expectedId: "google-gemini-cli" }, { requested: "gemini-cli", expectedId: "google-gemini-cli" }, + { requested: "openai-codex", expectedId: "openai" }, ] as const; for (const scenario of scenarios) { diff --git a/src/commands/models/auth-list.test.ts b/src/commands/models/auth-list.test.ts index 0c181621b6b9..39831b54f91d 100644 --- a/src/commands/models/auth-list.test.ts +++ b/src/commands/models/auth-list.test.ts @@ -147,7 +147,7 @@ describe("modelsAuthListCommand", () => { expect(mocks.externalCliDiscoveryForProviderAuth).toHaveBeenCalledWith({ cfg: {}, - provider: "openai-codex", + provider: "openai", }); expect(runtime.jsonPayloads).toStrictEqual([ { diff --git a/src/commands/models/auth-list.ts b/src/commands/models/auth-list.ts index 598223e6d4fd..ffd26b6852b6 100644 --- a/src/commands/models/auth-list.ts +++ b/src/commands/models/auth-list.ts @@ -42,7 +42,7 @@ function resolveProviderFilter(rawProvider: string | undefined): { if (provider === "openai") { return { provider, - externalCliProvider: "openai-codex", + externalCliProvider: "openai", matches: (profile) => profile.provider === "openai" || profile.provider === "openai-codex", }; } diff --git a/src/commands/models/auth.test.ts b/src/commands/models/auth.test.ts index 7d714fc4258f..fb5ab76222a4 100644 --- a/src/commands/models/auth.test.ts +++ b/src/commands/models/auth.test.ts @@ -493,9 +493,6 @@ describe("modelsAuthLoginCommand", () => { expect(runtime.log).toHaveBeenCalledWith( "Default model available: openai-codex/gpt-5.5 (use --set-default to apply)", ); - expect(runtime.log).toHaveBeenCalledWith( - "Tip: Codex-capable models can use native Codex web search. Enable it with openclaw configure --section web (recommended mode: cached). Docs: https://docs.openclaw.ai/tools/web", - ); }); it("defaults OpenAI login to ChatGPT OAuth when API key is also available", async () => { @@ -1230,10 +1227,10 @@ describe("modelsAuthLoginCommand", () => { await expect( modelsAuthPasteTokenCommand({ provider: "openai-codex" }, runtime), - ).rejects.toThrow("paste-api-key --provider openai-codex"); + ).rejects.toThrow("paste-api-key --provider openai"); expect(validateMessages).toEqual([ - "That looks like an OpenAI API key. Use openclaw models auth paste-api-key --provider openai-codex for API-key auth.", + "That looks like an OpenAI API key. Use openclaw models auth paste-api-key --provider openai for API-key auth.", ]); expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled(); expect(mocks.updateConfig).not.toHaveBeenCalled(); @@ -1246,7 +1243,7 @@ describe("modelsAuthLoginCommand", () => { await expect( modelsAuthPasteTokenCommand({ provider: "openai-codex" }, runtime), - ).rejects.toThrow("paste-api-key --provider openai-codex"); + ).rejects.toThrow("paste-api-key --provider openai"); expect(mocks.clackText).not.toHaveBeenCalled(); expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled(); @@ -1260,7 +1257,7 @@ describe("modelsAuthLoginCommand", () => { await expect( modelsAuthPasteTokenCommand({ provider: "openai-codex" }, runtime), - ).rejects.toThrow("paste-api-key --provider openai-codex"); + ).rejects.toThrow("paste-api-key --provider openai"); expect(mocks.clackText).not.toHaveBeenCalled(); expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled(); @@ -1276,21 +1273,19 @@ describe("modelsAuthLoginCommand", () => { expect(mocks.resolveDefaultAgentId).not.toHaveBeenCalled(); expect(mocks.upsertAuthProfileWithLock).toHaveBeenCalledWith({ - profileId: "openai-codex:manual", + profileId: "openai:manual", credential: { type: "api_key", - provider: "openai-codex", + provider: "openai", key: "sk-openai-codex-api-key-value", }, agentDir: "/tmp/openclaw/agents/coder", }); - expect(lastUpdatedConfig?.auth?.profiles?.["openai-codex:manual"]).toEqual({ - provider: "openai-codex", + expect(lastUpdatedConfig?.auth?.profiles?.["openai:manual"]).toEqual({ + provider: "openai", mode: "api_key", }); - expect(runtime.log).toHaveBeenCalledWith( - "Auth profile: openai-codex:manual (openai-codex/api_key)", - ); + expect(runtime.log).toHaveBeenCalledWith("Auth profile: openai:manual (openai/api_key)"); }); it("writes piped OpenAI Codex API keys to API-key profiles", async () => { @@ -1302,16 +1297,16 @@ describe("modelsAuthLoginCommand", () => { expect(mocks.clackPassword).not.toHaveBeenCalled(); expect(mocks.upsertAuthProfileWithLock).toHaveBeenCalledWith({ - profileId: "openai-codex:manual", + profileId: "openai:manual", credential: { type: "api_key", - provider: "openai-codex", + provider: "openai", key: "sk-openai-codex-api-key-value", }, agentDir: "/tmp/openclaw/agents/main", }); - expect(lastUpdatedConfig?.auth?.profiles?.["openai-codex:manual"]).toEqual({ - provider: "openai-codex", + expect(lastUpdatedConfig?.auth?.profiles?.["openai:manual"]).toEqual({ + provider: "openai", mode: "api_key", }); }); @@ -1325,16 +1320,16 @@ describe("modelsAuthLoginCommand", () => { expect(mocks.clackPassword).not.toHaveBeenCalled(); expect(mocks.upsertAuthProfileWithLock).toHaveBeenCalledWith({ - profileId: "openai-codex:manual", + profileId: "openai:manual", credential: { type: "api_key", - provider: "openai-codex", + provider: "openai", key: "sk-openai-codex-api-key-value", }, agentDir: "/tmp/openclaw/agents/main", }); - expect(lastUpdatedConfig?.auth?.profiles?.["openai-codex:manual"]).toEqual({ - provider: "openai-codex", + expect(lastUpdatedConfig?.auth?.profiles?.["openai:manual"]).toEqual({ + provider: "openai", mode: "api_key", }); }); @@ -1356,10 +1351,10 @@ describe("modelsAuthLoginCommand", () => { await expect( modelsAuthPasteApiKeyCommand({ provider: "openai-codex" }, runtime), - ).rejects.toThrow("paste-token --provider openai-codex"); + ).rejects.toThrow("paste-token --provider openai"); expect(validateMessages).toEqual([ - "That looks like token or OAuth material, not an OpenAI API key. Use openclaw models auth paste-token --provider openai-codex for token auth material.", + "That looks like token or OAuth material, not an OpenAI API key. Use openclaw models auth paste-token --provider openai for token auth material.", ]); expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled(); expect(mocks.updateConfig).not.toHaveBeenCalled(); diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index 6013f45c7fe9..316ed9fb7a8c 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -135,8 +135,13 @@ function resolveDefaultTokenProfileId(provider: string): string { return `${normalizeProviderId(provider)}:manual`; } -function isOpenAICodexProvider(provider: string): boolean { - return normalizeProviderId(provider) === "openai-codex"; +function normalizeManualAuthProvider(provider: string): string { + const normalized = normalizeProviderId(provider); + return normalized === "openai-codex" ? "openai" : normalized; +} + +function isOpenAIProvider(provider: string): boolean { + return normalizeProviderId(provider) === "openai"; } function stripBearerPrefix(value: string): string { @@ -170,7 +175,7 @@ function validateOpenAICodexApiKeyInput(value: string): string | undefined { return undefined; } if (looksLikeJwtToken(trimmed) || looksLikeStructuredCredential(trimmed)) { - return `That looks like token or OAuth material, not an OpenAI API key. Use ${formatCliCommand("openclaw models auth paste-token --provider openai-codex")} for token auth material.`; + return `That looks like token or OAuth material, not an OpenAI API key. Use ${formatCliCommand("openclaw models auth paste-token --provider openai")} for token auth material.`; } return "That does not look like an OpenAI API key."; } @@ -594,7 +599,7 @@ export async function modelsAuthPasteTokenCommand( `Missing --provider. Run ${formatCliCommand("openclaw models status")} or ${formatCliCommand("openclaw plugins list")} to choose a provider.`, ); } - const provider = normalizeProviderId(rawProvider); + const provider = normalizeManualAuthProvider(rawProvider); const profileId = normalizeOptionalString(opts.profileId) || resolveDefaultTokenProfileId(provider); @@ -606,8 +611,8 @@ export async function modelsAuthPasteTokenCommand( if (provider === "anthropic") { return validateAnthropicSetupToken(trimmed.replaceAll(/\s+/g, "")); } - if (isOpenAICodexProvider(provider) && looksLikeOpenAIApiKey(trimmed)) { - return `That looks like an OpenAI API key. Use ${formatCliCommand("openclaw models auth paste-api-key --provider openai-codex")} for API-key auth.`; + if (isOpenAIProvider(provider) && looksLikeOpenAIApiKey(trimmed)) { + return `That looks like an OpenAI API key. Use ${formatCliCommand("openclaw models auth paste-api-key --provider openai")} for API-key auth.`; } return undefined; }; @@ -665,7 +670,7 @@ export async function modelsAuthPasteApiKeyCommand( `Missing --provider. Run ${formatCliCommand("openclaw models status")} or ${formatCliCommand("openclaw plugins list")} to choose a provider.`, ); } - const provider = normalizeProviderId(rawProvider); + const provider = normalizeManualAuthProvider(rawProvider); const profileId = normalizeOptionalString(opts.profileId) || resolveDefaultTokenProfileId(provider); @@ -677,7 +682,7 @@ export async function modelsAuthPasteApiKeyCommand( if (!trimmed) { return "Required"; } - if (isOpenAICodexProvider(provider)) { + if (isOpenAIProvider(provider)) { return validateOpenAICodexApiKeyInput(trimmed); } return undefined; @@ -880,7 +885,7 @@ export function resolveLoginProfiles(params: { } function maybeLogOpenAICodexNativeSearchTip(runtime: RuntimeEnv, providerId: string) { - if (providerId !== "openai-codex") { + if (providerId !== "openai") { return; } runtime.log( diff --git a/src/commands/models/list.auth-index.ts b/src/commands/models/list.auth-index.ts index 4be93d69745f..962738710e5e 100644 --- a/src/commands/models/list.auth-index.ts +++ b/src/commands/models/list.auth-index.ts @@ -11,6 +11,7 @@ import { } from "../../agents/model-auth.js"; import { OPENAI_CODEX_PROVIDER_ID, + OPENAI_PROVIDER_ID, openAIProviderUsesCodexRuntimeByDefault, } from "../../agents/openai-codex-routing.js"; import { normalizeProviderIdForAuth } from "../../agents/provider-id.js"; @@ -41,6 +42,17 @@ function normalizeAuthProvider( return aliasMap[normalized] ?? normalized; } +function normalizeStoredAuthProvider( + provider: string, + aliasMap: Readonly>, +): string { + const normalized = normalizeProviderIdForAuth(provider); + if (normalized === OPENAI_CODEX_PROVIDER_ID) { + return normalized; + } + return aliasMap[normalized] ?? normalized; +} + function listValidatedSyntheticAuthProviderRefs(params: { cfg: OpenClawConfig; workspaceDir?: string; @@ -84,7 +96,7 @@ export function createModelListAuthIndex( if (!provider?.trim()) { return; } - authenticatedProviders.add(normalizeAuthProvider(provider, aliasMap)); + authenticatedProviders.add(normalizeStoredAuthProvider(provider, aliasMap)); }; const addSyntheticProvider = (provider: string | undefined) => { const normalized = provider?.trim() ? normalizeProviderIdForAuth(provider) : ""; @@ -128,7 +140,7 @@ export function createModelListAuthIndex( const primaryModelProvider = resolveAgentModelPrimaryValue( params.cfg.agents?.defaults?.model, )?.split("/", 1)[0]; - if (primaryModelProvider === "openai-codex" || primaryModelProvider === "codex") { + if (primaryModelProvider === "codex") { addSyntheticProvider("codex"); } @@ -175,7 +187,9 @@ export function createModelListAuthIndex( openAIProviderUsesCodexRuntimeByDefault({ provider: normalizedProvider, config: params.cfg, - }) && authenticatedProviders.has(OPENAI_CODEX_PROVIDER_ID) + }) && + (authenticatedProviders.has(OPENAI_PROVIDER_ID) || + authenticatedProviders.has(OPENAI_CODEX_PROVIDER_ID)) ); }; diff --git a/src/commands/models/list.probe.test.ts b/src/commands/models/list.probe.test.ts index cd8932b6f648..710ee088100e 100644 --- a/src/commands/models/list.probe.test.ts +++ b/src/commands/models/list.probe.test.ts @@ -94,8 +94,6 @@ describe("runAuthProbes", () => { expect(result.results[0]?.status).toBe("ok"); expect(runEmbeddedAgent).toHaveBeenCalledWith( expect.objectContaining({ - agentHarnessId: "openclaw", - agentHarnessRuntimeOverride: "openclaw", modelRun: true, disableTools: true, authProfileId: "openai-codex:profile", diff --git a/src/commands/models/list.probe.ts b/src/commands/models/list.probe.ts index ac079b5d413d..748f8927223c 100644 --- a/src/commands/models/list.probe.ts +++ b/src/commands/models/list.probe.ts @@ -522,9 +522,6 @@ async function probeTarget(params: { model: target.model.model, authProfileId: target.profileId, authProfileIdSource: target.profileId ? "user" : undefined, - ...(target.provider === "openai-codex" - ? { agentHarnessId: "openclaw", agentHarnessRuntimeOverride: "openclaw" } - : {}), timeoutMs, runId: `probe-${crypto.randomUUID()}`, lane: `auth-probe:${target.provider}:${target.profileId ?? target.source}`, diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index 40977fc9a5b4..b91731f51641 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -32,6 +32,7 @@ import { } from "../../agents/model-selection.js"; import { OPENAI_CODEX_PROVIDER_ID, + OPENAI_PROVIDER_ID, openAIProviderUsesCodexRuntimeByDefault, } from "../../agents/openai-codex-routing.js"; import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js"; @@ -424,7 +425,7 @@ export async function modelsStatusCommand( const syntheticProvidersToProbe = new Set( providers.map((provider) => normalizeProviderId(provider)), ); - const codexProvider = normalizeProviderId(OPENAI_CODEX_PROVIDER_ID); + const codexProvider = normalizeProviderId(OPENAI_PROVIDER_ID); const codexProviderAlias = aliasMap[codexProvider] ?? codexProvider; const codexRuntimeAuthUsages = providerUses.filter( (usage) => @@ -513,35 +514,61 @@ export async function modelsStatusCommand( }; const resolveProviderAuthHealthId = (provider: string): string => resolveProviderIdForAuth(provider, envLookupParams); + const listRuntimeAuthProviderCandidates = ( + provider: string, + options?: { includeLegacyOpenAICodex?: boolean }, + ): string[] => { + const normalizedProvider = normalizeProviderId(provider); + const candidates = [normalizedProvider, resolveProviderAuthHealthId(normalizedProvider)]; + if ( + options?.includeLegacyOpenAICodex === true && + openAIProviderUsesCodexRuntimeByDefault({ + provider: normalizedProvider, + config: cfg, + }) + ) { + candidates.push(OPENAI_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID); + } + return Array.from(new Set(candidates)); + }; const resolveRuntimeAuthRouteEffective = ( provider: string, ): ProviderAuthOverview["effective"] => { - const direct = providerAuthMap.get(provider)?.effective; - if (direct && direct.kind !== "missing") { - return direct; - } - const orderedProfiles = resolveAuthProfileOrder({ - cfg, - store, - provider, + const candidates = listRuntimeAuthProviderCandidates(provider, { + includeLegacyOpenAICodex: true, }); - const profileId = orderedProfiles[0]; - const credential = profileId ? store.profiles[profileId] : undefined; - if (profileId && credential) { - const sourceProvider = resolveProviderAuthHealthId(credential.provider); - const source = providerAuthMap.get(sourceProvider)?.effective; - return source && source.kind !== "missing" - ? source - : { - kind: "profiles", - detail: `${profileId} (${credential.provider})`, - }; + for (const candidate of candidates) { + const direct = providerAuthMap.get(candidate)?.effective; + if (direct && direct.kind !== "missing") { + return direct; + } } - return direct ?? missingProviderAuthEffective; + for (const candidate of candidates) { + const orderedProfiles = resolveAuthProfileOrder({ + cfg, + store, + provider: candidate, + }); + const profileId = orderedProfiles[0]; + const credential = profileId ? store.profiles[profileId] : undefined; + if (profileId && credential) { + const sourceProvider = resolveProviderAuthHealthId(credential.provider); + const source = providerAuthMap.get(sourceProvider)?.effective; + return source && source.kind !== "missing" + ? source + : { + kind: "profiles", + detail: `${profileId} (${credential.provider})`, + }; + } + } + return providerAuthMap.get(provider)?.effective ?? missingProviderAuthEffective; }; - const hasUsableNonProfileAuth = (provider: string): boolean => { - const authProvider = resolveProviderAuthHealthId(provider); - for (const candidate of new Set([provider, authProvider])) { + const hasUsableNonProfileAuth = ( + provider: string, + options?: { includeLegacyOpenAICodex?: boolean }, + ): boolean => { + for (const candidate of listRuntimeAuthProviderCandidates(provider, options)) { const auth = providerAuthMap.get(candidate); if ( auth?.env || @@ -554,9 +581,11 @@ export async function modelsStatusCommand( } return false; }; - const hasUsableProviderAuth = (provider: string): boolean => { - const authProvider = resolveProviderAuthHealthId(provider); - for (const candidate of new Set([provider, authProvider])) { + const hasUsableProviderAuth = ( + provider: string, + options?: { includeLegacyOpenAICodex?: boolean }, + ): boolean => { + for (const candidate of listRuntimeAuthProviderCandidates(provider, options)) { const orderedProfiles = resolveAuthProfileOrder({ cfg, store, @@ -580,7 +609,7 @@ export async function modelsStatusCommand( } return ( openAIProviderUsesCodexRuntimeByDefault({ provider, config: cfg }) && - hasUsableProviderAuth(OPENAI_CODEX_PROVIDER_ID) + hasUsableProviderAuth(OPENAI_PROVIDER_ID, { includeLegacyOpenAICodex: true }) ); }; const runtimeAuthRoutes = Array.from( @@ -593,7 +622,11 @@ export async function modelsStatusCommand( provider: usage.provider, runtime: "codex", authProvider: codexProvider, - status: hasUsableProviderAuth(codexProvider) ? "usable" : "missing", + status: hasUsableProviderAuth(codexProvider, { + includeLegacyOpenAICodex: true, + }) + ? "usable" + : "missing", effective, }, ] as const; @@ -755,9 +788,13 @@ export async function modelsStatusCommand( if ( usage.allowCodexRuntimeFallback && openAIProviderUsesCodexRuntimeByDefault({ provider: usage.provider, config: cfg }) && - hasUsableProviderAuth(OPENAI_CODEX_PROVIDER_ID) + hasUsableProviderAuth(OPENAI_PROVIDER_ID, { includeLegacyOpenAICodex: true }) ) { - providersInUse.add(OPENAI_CODEX_PROVIDER_ID); + for (const candidate of listRuntimeAuthProviderCandidates(OPENAI_PROVIDER_ID, { + includeLegacyOpenAICodex: true, + })) { + providersInUse.add(candidate); + } } } const hasExpiredOrMissing = @@ -1042,7 +1079,9 @@ export async function modelsStatusCommand( const usageProviders = Array.from( new Set( oauthProfiles - .map((profile) => resolveUsageProviderId(profile.provider)) + .map((profile) => + resolveUsageProviderId(profile.provider, { credentialType: profile.type }), + ) .filter((provider): provider is NonNullable => Boolean(provider)), ), ); @@ -1095,7 +1134,12 @@ export async function modelsStatusCommand( } for (const [provider, profiles] of profilesByProvider) { - const usageKey = resolveUsageProviderId(provider); + const usageProfile = profiles.find( + (profile) => profile.type === "oauth" || profile.type === "token", + ); + const usageKey = resolveUsageProviderId(provider, { + credentialType: usageProfile?.type, + }); const usage = usageKey ? usageByProvider.get(usageKey) : undefined; const usageSuffix = usage ? colorize(rich, theme.muted, ` usage: ${usage}`) : ""; runtime.log(`- ${colorize(rich, theme.heading, provider)}${usageSuffix}`); diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.test.ts index b3d87eec06a3..5433ec2506ea 100644 --- a/src/commands/models/list.status.test.ts +++ b/src/commands/models/list.status.test.ts @@ -611,11 +611,11 @@ describe("modelsStatusCommand auth overview", () => { { provider: "openai", runtime: "codex", - authProvider: "openai-codex", + authProvider: "openai", status: "missing", effective: { - kind: "missing", - detail: "missing", + kind: "models.json", + detail: "marker(oauth:openai-codex)", }, }, ]); @@ -692,7 +692,7 @@ describe("modelsStatusCommand auth overview", () => { { provider: "openai", runtime: "codex", - authProvider: "openai-codex", + authProvider: "openai", status: "usable", effective: { kind: "synthetic", @@ -763,7 +763,7 @@ describe("modelsStatusCommand auth overview", () => { { provider: "openai", runtime: "codex", - authProvider: "openai-codex", + authProvider: "openai", status: "usable", effective: { kind: "profiles", diff --git a/src/commands/oauth-tls-preflight.doctor.test.ts b/src/commands/oauth-tls-preflight.doctor.test.ts index 6ed7322dcc93..ca9c50b6ad99 100644 --- a/src/commands/oauth-tls-preflight.doctor.test.ts +++ b/src/commands/oauth-tls-preflight.doctor.test.ts @@ -23,6 +23,20 @@ function buildOpenAICodexOAuthConfig(): OpenClawConfig { }; } +function buildOpenAIOAuthConfig(): OpenClawConfig { + return { + auth: { + profiles: { + "openai:user@example.com": { + provider: "openai", + mode: "oauth", + email: "user@example.com", + }, + }, + }, + }; +} + describe("noteOpenAIOAuthTlsPrerequisites", () => { beforeEach(() => { note.mockClear(); @@ -63,6 +77,21 @@ describe("noteOpenAIOAuthTlsPrerequisites", () => { expect(note).not.toHaveBeenCalled(); }); + it("runs the preflight for canonical OpenAI OAuth profiles", async () => { + const fetchMock = vi.fn(async () => new Response("", { status: 400 })); + const originalFetch = globalThis.fetch; + vi.stubGlobal("fetch", fetchMock); + + try { + await noteOpenAIOAuthTlsPrerequisites({ cfg: buildOpenAIOAuthConfig() }); + } finally { + vi.stubGlobal("fetch", originalFetch); + } + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(note).not.toHaveBeenCalled(); + }); + it("skips probe when OpenAI Codex OAuth is not configured", async () => { const fetchMock = vi.fn(async () => new Response("", { status: 400 })); const originalFetch = globalThis.fetch; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.test.ts index ce50ca12e2d9..248274775fff 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.test.ts @@ -22,6 +22,13 @@ vi.mock("../../../plugins/provider-auth-choices.js", () => ({ beforeEach(() => { vi.clearAllMocks(); + applyNonInteractivePluginProviderChoice.mockReset(); + applyNonInteractivePluginProviderChoice.mockResolvedValue(undefined); + resolveNonInteractiveApiKey.mockReset(); + resolveManifestDeprecatedProviderAuthChoice.mockReset(); + resolveManifestDeprecatedProviderAuthChoice.mockReturnValue(undefined); + resolveManifestProviderAuthChoices.mockReset(); + resolveManifestProviderAuthChoices.mockReturnValue([]); }); function createRuntime() { @@ -54,7 +61,7 @@ describe("applyNonInteractiveAuthChoice", () => { it("fails with manifest-owned replacement guidance for deprecated auth choices", async () => { const runtime = createRuntime(); const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; - resolveManifestDeprecatedProviderAuthChoice.mockReturnValueOnce({ + resolveManifestDeprecatedProviderAuthChoice.mockReturnValue({ choiceId: "demo-provider-modern-api", } as never); @@ -74,6 +81,37 @@ describe("applyNonInteractiveAuthChoice", () => { expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledOnce(); }); + it("normalizes legacy OpenAI Codex setup choice before provider auth", async () => { + const runtime = createRuntime(); + const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; + const resolvedConfig = { auth: { profiles: { "openai:default": { mode: "oauth" } } } }; + resolveManifestDeprecatedProviderAuthChoice.mockImplementation(((choiceId: string) => + choiceId === "openai-codex" + ? { + choiceId: "openai", + choiceLabel: "ChatGPT Login", + } + : undefined) as never); + applyNonInteractivePluginProviderChoice.mockResolvedValueOnce(resolvedConfig as never); + + const result = await applyNonInteractiveAuthChoice({ + nextConfig, + authChoice: "openai-codex", + opts: {} as never, + runtime: runtime as never, + baseConfig: nextConfig, + }); + + expect(result).toBe(resolvedConfig); + expect(runtime.log).toHaveBeenCalledWith( + 'Auth choice "openai-codex" is deprecated; using ChatGPT Login setup instead.', + ); + expect(runtime.error).not.toHaveBeenCalled(); + expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledWith( + expect.objectContaining({ authChoice: "openai" }), + ); + }); + it("escapes deprecated auth choice guidance for terminal output", async () => { const runtime = createRuntime(); const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 7de0a0e075ba..7201475b6ace 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -9,6 +9,7 @@ import { resolveDefaultSecretProviderAlias } from "../../../secrets/ref-contract import { formatDeprecatedNonInteractiveAuthChoiceError, isDeprecatedAuthChoice, + resolveDeprecatedAuthChoiceReplacement, } from "../../auth-choice-legacy.js"; import { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js"; import { normalizeApiKeyTokenProviderAuthChoice } from "../../auth-choice.apply.api-providers.js"; @@ -34,7 +35,7 @@ export async function applyNonInteractiveAuthChoice(params: { baseConfig: OpenClawConfig; }): Promise { const { opts, runtime, baseConfig } = params; - const authChoice = normalizeApiKeyTokenProviderAuthChoice({ + let authChoice = normalizeApiKeyTokenProviderAuthChoice({ authChoice: params.authChoice, tokenProvider: opts.tokenProvider, config: params.nextConfig, @@ -121,14 +122,23 @@ export async function applyNonInteractiveAuthChoice(params: { }; }; if (isDeprecatedAuthChoice(authChoice, { config: nextConfig, env: process.env })) { - runtime.error( - formatDeprecatedNonInteractiveAuthChoiceError(authChoice, { - config: nextConfig, - env: process.env, - })!, - ); - runtime.exit(1); - return null; + const replacement = resolveDeprecatedAuthChoiceReplacement(authChoice, { + config: nextConfig, + env: process.env, + }); + if (replacement) { + runtime.log(replacement.message); + authChoice = replacement.normalized; + } else { + runtime.error( + formatDeprecatedNonInteractiveAuthChoiceError(authChoice, { + config: nextConfig, + env: process.env, + })!, + ); + runtime.exit(1); + return null; + } } const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({ diff --git a/src/commands/provider-auth-guidance.ts b/src/commands/provider-auth-guidance.ts index b37b358fe463..4b5d60d27df9 100644 --- a/src/commands/provider-auth-guidance.ts +++ b/src/commands/provider-auth-guidance.ts @@ -37,7 +37,8 @@ function resolveProviderAuthLoginCommand(params: { if (!choice) { return undefined; } - return formatCliCommand(`openclaw models auth login --provider ${choice.providerId}`); + const providerId = normalizeProviderIdForAuth(choice.providerId, aliases); + return formatCliCommand(`openclaw models auth login --provider ${providerId}`); } export function buildProviderAuthRecoveryHint(params: { diff --git a/src/gateway/server-methods/models-auth-status.ts b/src/gateway/server-methods/models-auth-status.ts index 3fa8c3312829..180feca0b0c5 100644 --- a/src/gateway/server-methods/models-auth-status.ts +++ b/src/gateway/server-methods/models-auth-status.ts @@ -238,7 +238,12 @@ function mapProvider( usageByProvider: Map, expectsOAuthSet: Set, ): ModelAuthStatusProvider { - const usageKey = resolveUsageProviderId(prov.provider); + const usageProfile = prov.profiles.find( + (profile) => profile.type === "oauth" || profile.type === "token", + ); + const usageKey = resolveUsageProviderId(prov.provider, { + credentialType: usageProfile?.type, + }); const usage = usageKey ? usageByProvider.get(usageKey) : undefined; const rollup = aggregateOAuthStatus(prov, Date.now(), expectsOAuthSet.has(prov.provider)); return { @@ -432,7 +437,7 @@ export const modelsAuthStatusHandlers: GatewayRequestHandlers = { ...new Set( authHealth.profiles .filter((p) => p.type === "oauth" || p.type === "token") - .map((p) => resolveUsageProviderId(p.provider)) + .map((p) => resolveUsageProviderId(p.provider, { credentialType: p.type })) .filter((id): id is UsageProviderId => Boolean(id)), ), ]; diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 2c206cc849ac..eca848075971 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -517,7 +517,7 @@ describe("gateway session utils", () => { }); test("session defaults and rows expose bundled startup-lazy provider thinking without catalog", () => { - const cfg = createModelDefaultsConfig({ primary: "openai-codex/gpt-5.5" }); + const cfg = createModelDefaultsConfig({ primary: "openai/gpt-5.5" }); const defaults = getSessionDefaults(cfg); const row = buildGatewaySessionRow({ @@ -535,14 +535,14 @@ describe("gateway session utils", () => { const defaults = getSessionDefaults({ agents: { defaults: { - model: { primary: "openai-codex/gpt-5.5" }, + model: { primary: "openai/gpt-5.5" }, thinkingDefault: "high", }, }, } as OpenClawConfig); expectFields(defaults, { - modelProvider: "openai-codex", + modelProvider: "openai", model: "gpt-5.5", thinkingDefault: "high", }); @@ -634,10 +634,10 @@ describe("gateway session utils", () => { const cfg = { agents: { defaults: { - model: { primary: "openai-codex/gpt-5.5" }, + model: { primary: "openai/gpt-5.5" }, thinkingDefault: "low", models: { - "openai-codex/gpt-5.5": { + "openai/gpt-5.5": { params: { thinking: "max" }, }, }, @@ -660,7 +660,7 @@ describe("gateway session utils", () => { }); expectFields(row, { - modelProvider: "openai-codex", + modelProvider: "openai", model: "gpt-5.5", thinkingDefault: "high", }); @@ -670,10 +670,10 @@ describe("gateway session utils", () => { const cfg = { agents: { defaults: { - model: { primary: "openai-codex/gpt-5.5" }, + model: { primary: "openai/gpt-5.5" }, thinkingDefault: "low", models: { - "openai-codex/gpt-5.5": { + "openai/gpt-5.5": { params: { thinking: "max" }, }, }, @@ -689,7 +689,7 @@ describe("gateway session utils", () => { }); expectFields(row, { - modelProvider: "openai-codex", + modelProvider: "openai", model: "gpt-5.5", thinkingDefault: "max", }); diff --git a/src/gateway/sessions-patch.test.ts b/src/gateway/sessions-patch.test.ts index d4975ce7b633..aed7d8b7c51c 100644 --- a/src/gateway/sessions-patch.test.ts +++ b/src/gateway/sessions-patch.test.ts @@ -646,7 +646,7 @@ describe("gateway sessions patch", () => { cfg: { agents: { defaults: { - model: { primary: "openai-codex/gpt-5.5" }, + model: { primary: "openai/gpt-5.5" }, }, }, } as OpenClawConfig, diff --git a/src/infra/provider-usage.shared.test.ts b/src/infra/provider-usage.shared.test.ts index ee717f2c2767..4b6279cc54d5 100644 --- a/src/infra/provider-usage.shared.test.ts +++ b/src/infra/provider-usage.shared.test.ts @@ -22,6 +22,12 @@ describe("provider-usage.shared", () => { expect(resolveUsageProviderId(value)).toBe(expected); }); + it("maps canonical OpenAI subscription profiles to Codex usage windows", () => { + expect(resolveUsageProviderId("openai", { credentialType: "oauth" })).toBe("openai-codex"); + expect(resolveUsageProviderId("openai", { credentialType: "token" })).toBe("openai-codex"); + expect(resolveUsageProviderId("openai", { credentialType: "api_key" })).toBeUndefined(); + }); + it.each([ { value: -5, expected: 0 }, { value: 42, expected: 42 }, diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index d80c5ac029fd..7356f4ed9f96 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -25,11 +25,20 @@ export const usageProviders: UsageProviderId[] = [ "zai", ]; -export function resolveUsageProviderId(provider?: string | null): UsageProviderId | undefined { +export function resolveUsageProviderId( + provider?: string | null, + options?: { credentialType?: string | null }, +): UsageProviderId | undefined { if (!provider) { return undefined; } const normalized = normalizeProviderId(provider); + if ( + normalized === "openai" && + (options?.credentialType === "oauth" || options?.credentialType === "token") + ) { + return "openai-codex"; + } if ( normalized === "minimax-portal" || normalized === "minimax-cn" || diff --git a/src/llm/providers/stream-wrappers/openai.test.ts b/src/llm/providers/stream-wrappers/openai.test.ts index 88205b8cc218..ad8a5e968764 100644 --- a/src/llm/providers/stream-wrappers/openai.test.ts +++ b/src/llm/providers/stream-wrappers/openai.test.ts @@ -26,7 +26,7 @@ function createPayloadCapture(opts?: { initialReasoning?: unknown }) { const codexModel = { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.1-codex", } as Model<"openai-codex-responses">; @@ -493,7 +493,7 @@ describe("createOpenAIThinkingLevelWrapper", () => { }, { api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", id: "gpt-5.5", }, ] as const)("preserves xhigh for $provider/$id", (model) => { diff --git a/src/llm/providers/stream-wrappers/openai.ts b/src/llm/providers/stream-wrappers/openai.ts index 7ef31177a1ca..0f98a1951b22 100644 --- a/src/llm/providers/stream-wrappers/openai.ts +++ b/src/llm/providers/stream-wrappers/openai.ts @@ -83,6 +83,19 @@ function shouldApplyOpenAIAttributionHeaders(model: { : undefined; } +function shouldUseCodexNativeTransport(model: { + api?: unknown; + provider?: unknown; + baseUrl?: unknown; + compat?: unknown; +}): boolean { + const api = readStringValue(model.api); + if (api !== "openai-codex-responses") { + return false; + } + return resolveOpenAIRequestCapabilities(model).endpointClass === "openai-codex"; +} + function shouldApplyOpenAIServiceTier(model: { api?: unknown; provider?: unknown; @@ -694,7 +707,7 @@ export function createOpenAIAttributionHeadersWrapper( return underlying(model, context, options); } const shouldCreateCodexTransport = - attributionProvider === "openai-codex" && + shouldUseCodexNativeTransport(model) && (baseStreamFn === undefined || baseStreamFn === streamSimple); const streamFn = shouldCreateCodexTransport ? (opts?.codexNativeTransportStreamFn ?? createOpenAIResponsesTransportStreamFn()) diff --git a/src/llm/utils/oauth/openai-codex.test.ts b/src/llm/utils/oauth/openai-codex.test.ts index 606731ead828..5eeecd141edf 100644 --- a/src/llm/utils/oauth/openai-codex.test.ts +++ b/src/llm/utils/oauth/openai-codex.test.ts @@ -154,10 +154,10 @@ describe("OpenAI Codex OAuth compatibility provider", () => { }); expect(mocks.refreshProviderOAuthCredentialWithPlugin).toHaveBeenCalledWith({ - provider: "openai-codex", + provider: "openai", context: { type: "oauth", - provider: "openai-codex", + provider: "openai", access: "", refresh: "old-refresh-token", expires: 0, diff --git a/src/llm/utils/oauth/openai-codex.ts b/src/llm/utils/oauth/openai-codex.ts index 9971f128465c..4eb581a3f144 100644 --- a/src/llm/utils/oauth/openai-codex.ts +++ b/src/llm/utils/oauth/openai-codex.ts @@ -4,7 +4,7 @@ import type { WizardPrompter } from "../../../wizard/prompts.js"; import { throwIfOAuthLoginAborted, withOAuthLoginAbort } from "./abort.js"; import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js"; -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const OPENAI_CODEX_PROVIDER_ID = "openai"; type OpenAICodexOAuthFacade = { refreshOpenAICodexToken: (refreshToken: string) => Promise; diff --git a/src/plugin-sdk/provider-auth.ts b/src/plugin-sdk/provider-auth.ts index 39b2fc9eb5cb..60f7b4a59ccd 100644 --- a/src/plugin-sdk/provider-auth.ts +++ b/src/plugin-sdk/provider-auth.ts @@ -12,6 +12,7 @@ import { loadAuthProfileStoreWithoutExternalProfiles, } from "../agents/auth-profiles/store.js"; import type { AuthProfileStore } from "../agents/auth-profiles/types.js"; +import type { AuthProfileCredential } from "../agents/auth-profiles/types.js"; import { COPILOT_INTEGRATION_ID, buildCopilotIdeHeaders, @@ -279,6 +280,7 @@ export async function resolveCopilotApiToken(params: { export function isProviderApiKeyConfigured(params: { provider: string; agentDir?: string; + profileTypes?: readonly AuthProfileCredential["type"][]; }): boolean { if (resolveEnvApiKey(params.provider)?.apiKey) { return true; @@ -290,7 +292,15 @@ export function isProviderApiKeyConfigured(params: { const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false, }); - return listProfilesForProvider(store, params.provider).length > 0; + const profileIds = listProfilesForProvider(store, params.provider); + if (!params.profileTypes?.length) { + return profileIds.length > 0; + } + const allowedTypes = new Set(params.profileTypes); + return profileIds.some((profileId) => { + const type = store.profiles[profileId]?.type; + return type !== undefined && allowedTypes.has(type); + }); } export function listUsableProviderAuthProfileIds(params: { diff --git a/src/plugin-sdk/provider-tools.test.ts b/src/plugin-sdk/provider-tools.test.ts index d39774706e70..0290bb7c4b7c 100644 --- a/src/plugin-sdk/provider-tools.test.ts +++ b/src/plugin-sdk/provider-tools.test.ts @@ -56,6 +56,31 @@ describe("buildProviderToolCompatFamilyHooks", () => { } }); + it("normalizes canonical OpenAI Codex Responses tool schemas", () => { + const hooks = buildProviderToolCompatFamilyHooks("openai"); + const tools = [{ name: "demo", description: "", parameters: {} }] as never; + + const normalized = hooks.normalizeToolSchemas({ + provider: "openai", + modelId: "gpt-5.4", + modelApi: "openai-codex-responses", + model: { + provider: "openai", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + id: "gpt-5.4", + } as never, + tools, + }); + + expect(normalized[0]?.parameters).toEqual({ + type: "object", + properties: {}, + required: [], + additionalProperties: false, + }); + }); + it("collapses anyOf and oneOf unions for the deepseek family", () => { const hooks = buildProviderToolCompatFamilyHooks("deepseek"); const tools = [ diff --git a/src/plugin-sdk/provider-tools.ts b/src/plugin-sdk/provider-tools.ts index c88f23098f15..826355f4c37e 100644 --- a/src/plugin-sdk/provider-tools.ts +++ b/src/plugin-sdk/provider-tools.ts @@ -120,7 +120,13 @@ function shouldApplyOpenAIToolCompat(ctx: ProviderNormalizeToolSchemasContext): const baseUrl = (ctx.model?.baseUrl ?? "").trim().toLowerCase(); if (provider === "openai") { - return api === "openai-responses" && (!baseUrl || isOpenAIResponsesBaseUrl(baseUrl)); + if (api === "openai-responses") { + return !baseUrl || isOpenAIResponsesBaseUrl(baseUrl); + } + return ( + api === "openai-codex-responses" && + (!baseUrl || isOpenAIResponsesBaseUrl(baseUrl) || isOpenAICodexBaseUrl(baseUrl)) + ); } if (provider === "openai-codex") { return ( diff --git a/src/plugin-sdk/test-helpers/agents/auth-profile-runtime-contract.ts b/src/plugin-sdk/test-helpers/agents/auth-profile-runtime-contract.ts index 38cf16375620..d3e75a08bf3e 100644 --- a/src/plugin-sdk/test-helpers/agents/auth-profile-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/agents/auth-profile-runtime-contract.ts @@ -33,12 +33,21 @@ export function createAuthAliasManifestRegistry(): PluginManifestRegistry { rootDir: "/tmp/openclaw-auth-contract-plugin", source: "test", manifestPath: "/tmp/openclaw-auth-contract-plugin/plugin.json", + providerAuthAliases: { + [AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider]: + AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, + [AUTH_PROFILE_RUNTIME_CONTRACT.codexCliProvider]: + AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, + }, providerAuthChoices: [ { - provider: AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider, + provider: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, method: "oauth", - choiceId: AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider, - deprecatedChoiceIds: [AUTH_PROFILE_RUNTIME_CONTRACT.codexCliProvider], + choiceId: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, + deprecatedChoiceIds: [ + AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider, + AUTH_PROFILE_RUNTIME_CONTRACT.codexCliProvider, + ], }, ], }, diff --git a/src/plugin-sdk/test-helpers/plugin-registration-contract-cases.ts b/src/plugin-sdk/test-helpers/plugin-registration-contract-cases.ts index aa1602fae0bf..55f9a74b18d0 100644 --- a/src/plugin-sdk/test-helpers/plugin-registration-contract-cases.ts +++ b/src/plugin-sdk/test-helpers/plugin-registration-contract-cases.ts @@ -101,11 +101,11 @@ export const pluginRegistrationContractCases = { }, openai: { pluginId: "openai", - providerIds: ["openai", "openai-codex"], + providerIds: ["openai"], speechProviderIds: ["openai"], realtimeTranscriptionProviderIds: ["openai"], realtimeVoiceProviderIds: ["openai"], - mediaUnderstandingProviderIds: ["openai", "openai-codex"], + mediaUnderstandingProviderIds: ["openai"], imageGenerationProviderIds: ["openai"], requireSpeechVoices: true, requireDescribeImages: true, diff --git a/src/plugin-sdk/test-helpers/provider-auth-contract.ts b/src/plugin-sdk/test-helpers/provider-auth-contract.ts index 4e00f05f1b79..6b1311d181e0 100644 --- a/src/plugin-sdk/test-helpers/provider-auth-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-auth-contract.ts @@ -86,7 +86,7 @@ function buildOpenAICodexOAuthResult(params: { profileId: params.profileId, credential: { type: "oauth" as const, - provider: "openai-codex", + provider: "openai", access: params.access, refresh: params.refresh, expires: params.expires, @@ -147,7 +147,7 @@ export function describeOpenAICodexProviderAuthContract( }; const { loginOpenAICodexOAuthMock } = options; - describe("openai-codex provider auth contract", () => { + describe("openai provider ChatGPT auth contract", () => { installSharedAuthProfileStoreHooks(state); async function expectStableFallbackProfile(params: { access: string; profileId: string }) { @@ -187,7 +187,7 @@ export function describeOpenAICodexProviderAuthContract( expect(result).toEqual( buildOpenAICodexOAuthResult({ - profileId: "openai-codex:user@example.com", + profileId: "openai:user@example.com", access: "access-token", refresh: "refresh-token", expires: 1_700_000_000_000, @@ -213,7 +213,7 @@ export function describeOpenAICodexProviderAuthContract( expect(result).toEqual( buildOpenAICodexOAuthResult({ - profileId: "openai-codex:jwt-user@example.com", + profileId: "openai:jwt-user@example.com", access, refresh: "refresh-token", expires: 1_700_000_000_000, @@ -231,7 +231,7 @@ export function describeOpenAICodexProviderAuthContract( const expectedStableId = Buffer.from("user-123__acct-456", "utf8").toString("base64url"); await expectStableFallbackProfile({ access, - profileId: `openai-codex:id-${expectedStableId}`, + profileId: `openai:id-${expectedStableId}`, }); }); @@ -245,7 +245,7 @@ export function describeOpenAICodexProviderAuthContract( ); await expectStableFallbackProfile({ access, - profileId: `openai-codex:id-${expectedStableId}`, + profileId: `openai:id-${expectedStableId}`, }); }); @@ -256,7 +256,7 @@ export function describeOpenAICodexProviderAuthContract( const expectedStableId = Buffer.from("user-abc").toString("base64url"); await expectStableFallbackProfile({ access, - profileId: `openai-codex:id-${expectedStableId}`, + profileId: `openai:id-${expectedStableId}`, }); }); @@ -272,7 +272,7 @@ export function describeOpenAICodexProviderAuthContract( expect(result).toEqual( buildOpenAICodexOAuthResult({ - profileId: "openai-codex:default", + profileId: "openai:default", access: "not-a-jwt-token", refresh: "refresh-token", expires: 1_700_000_000_000, diff --git a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts index e2b3c97fe081..40c61499c4fa 100644 --- a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts @@ -570,7 +570,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr expectFields(model, { id: "gpt-5.4", - provider: "openai-codex", + provider: "openai", api: "openai-codex-responses", contextWindow: 1_050_000, maxTokens: 128_000, @@ -588,7 +588,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr ? createModel({ id, api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", baseUrl: "https://chatgpt.com/backend-api", input: ["text", "image"], cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, @@ -601,7 +601,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr expectFields(model, { id: "gpt-5.5", - provider: "openai-codex", + provider: "openai", api: "openai-codex-responses", contextWindow: 400_000, contextTokens: 272_000, @@ -620,7 +620,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr ? createModel({ id, api: "openai-codex-responses", - provider: "openai-codex", + provider: "openai", baseUrl: "https://chatgpt.com/backend-api", cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, contextWindow: 272_000, @@ -632,7 +632,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr expectFields(model, { id: "gpt-5.4-mini", - provider: "openai-codex", + provider: "openai", api: "openai-codex-responses", contextWindow: 400_000, contextTokens: 272_000, diff --git a/src/plugins/plugin-lookup-table.test.ts b/src/plugins/plugin-lookup-table.test.ts index af83d8dd384e..80f071bbfeac 100644 --- a/src/plugins/plugin-lookup-table.test.ts +++ b/src/plugins/plugin-lookup-table.test.ts @@ -185,7 +185,10 @@ describe("loadPluginLookUpTable", () => { createManifestRecord({ id: "openai", origin: "bundled", - providers: ["openai", "openai-codex"], + providers: ["openai"], + providerAuthAliases: { + "openai-codex": "openai", + }, modelCatalog: { aliases: { "azure-openai-responses": { @@ -248,6 +251,7 @@ describe("loadPluginLookUpTable", () => { expect(table.owners.channels.get("telegram")).toEqual(["telegram"]); expect(table.owners.channelConfigs.get("telegram")).toEqual(["telegram"]); expect(table.owners.providers.get("openai")).toEqual(["openai"]); + expect(table.owners.providers.get("openai-codex")).toEqual(["openai"]); expect(table.owners.modelCatalogProviders.get("openai")).toEqual(["openai"]); expect(table.owners.modelCatalogProviders.get("azure-openai-responses")).toEqual(["openai"]); expect(table.owners.cliBackends.get("codex-cli")).toBeUndefined(); diff --git a/src/plugins/plugin-metadata-snapshot.ts b/src/plugins/plugin-metadata-snapshot.ts index 7fb6e0cce253..a3c1b343bdbc 100644 --- a/src/plugins/plugin-metadata-snapshot.ts +++ b/src/plugins/plugin-metadata-snapshot.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { normalizeProviderId } from "../agents/provider-id.js"; import { resolveIsNixMode } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { @@ -462,6 +463,19 @@ function buildPluginMetadataOwnerMaps( for (const providerId of plugin.providers ?? []) { appendOwner(providers, providerId, plugin.id); } + for (const [rawAlias, target] of Object.entries(plugin.providerAuthAliases ?? {})) { + const alias = normalizeProviderId(rawAlias); + const targetProvider = normalizeProviderId(target); + if ( + alias && + targetProvider && + (plugin.providers ?? []).some( + (providerId) => normalizeProviderId(providerId) === targetProvider, + ) + ) { + appendOwner(providers, alias, plugin.id); + } + } for (const providerId of Object.keys(plugin.modelCatalog?.providers ?? {})) { appendOwner(modelCatalogProviders, providerId, plugin.id); } diff --git a/src/plugins/plugin-registry-id-normalizer.ts b/src/plugins/plugin-registry-id-normalizer.ts index fe7bd72af966..f35a79eafc32 100644 --- a/src/plugins/plugin-registry-id-normalizer.ts +++ b/src/plugins/plugin-registry-id-normalizer.ts @@ -29,6 +29,7 @@ function listPluginRegistryNormalizerAliases(plugin: PluginManifestRecord): read ...(plugin.setup?.cliBackends ?? []), ...collectObjectKeys(plugin.modelCatalog?.providers), ...collectObjectKeys(plugin.modelCatalog?.aliases), + ...collectObjectKeys(plugin.providerAuthAliases), ...(plugin.legacyPluginIds ?? []), ]; } diff --git a/src/plugins/provider-openai-codex-oauth-tls.ts b/src/plugins/provider-openai-codex-oauth-tls.ts index 29546a2d9104..a51da75d267a 100644 --- a/src/plugins/provider-openai-codex-oauth-tls.ts +++ b/src/plugins/provider-openai-codex-oauth-tls.ts @@ -83,7 +83,9 @@ function hasOpenAICodexOAuthProfile(cfg: OpenClawConfig): boolean { return false; } return Object.values(profiles).some( - (profile) => profile.provider === "openai-codex" && profile.mode === "oauth", + (profile) => + (profile.provider === "openai" || profile.provider === "openai-codex") && + profile.mode === "oauth", ); } diff --git a/src/plugins/provider-openai-codex-oauth.ts b/src/plugins/provider-openai-codex-oauth.ts index a6b23bb9f6d4..5d936d29cb3b 100644 --- a/src/plugins/provider-openai-codex-oauth.ts +++ b/src/plugins/provider-openai-codex-oauth.ts @@ -6,7 +6,7 @@ import { resolveProviderRuntimePlugin } from "./provider-hook-runtime.js"; import { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js"; import type { ProviderAuthContext } from "./types.js"; -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const OPENAI_CODEX_PROVIDER_ID = "openai"; const OPENAI_CODEX_OAUTH_METHOD_ID = "oauth"; type OpenAICodexOAuthBridgeContext = ProviderAuthContext & { diff --git a/src/plugins/provider-public-artifacts.test.ts b/src/plugins/provider-public-artifacts.test.ts index 8e381fe593d4..53356bac4fe8 100644 --- a/src/plugins/provider-public-artifacts.test.ts +++ b/src/plugins/provider-public-artifacts.test.ts @@ -22,6 +22,7 @@ describe("provider public artifacts", () => { process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir; } vi.doUnmock("./bundled-dir.js"); + vi.doUnmock("./manifest-registry.js"); vi.doUnmock("./public-surface-loader.js"); vi.resetModules(); }); @@ -117,6 +118,68 @@ describe("provider public artifacts", () => { } }); + it("resolves bundled policy artifacts through provider auth aliases", async () => { + const loadPluginManifestRegistry = vi.fn(() => { + throw new Error("unexpected manifest registry scan"); + }); + const loadBundledPluginPublicArtifactModuleSync = vi.fn(({ dirName }: { dirName: string }) => { + if (dirName !== "openai") { + throw new Error(`Unable to resolve bundled plugin public surface ${dirName}`); + } + return { + resolveThinkingProfile: ({ provider }: { provider: string }) => ({ + levels: [{ id: provider }], + }), + }; + }); + + vi.doMock("./manifest-registry.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadPluginManifestRegistry, + }; + }); + vi.doMock("./public-surface-loader.js", () => ({ + loadBundledPluginPublicArtifactModuleSync, + })); + + const { resolveBundledProviderPolicySurface: resolvePolicySurface } = await importFreshModule< + typeof import("./provider-public-artifacts.js") + >(import.meta.url, "./provider-public-artifacts.js?scope=provider-auth-alias"); + + const surface = resolvePolicySurface("openai-codex", { + manifestRegistry: { + plugins: [ + { + id: "openai", + channels: [], + cliBackends: [], + hooks: [], + origin: "bundled", + manifestPath: "/tmp/openai/openclaw.plugin.json", + providers: ["openai"], + providerAuthAliases: { "openai-codex": "openai" }, + rootDir: "/tmp/openai", + skills: [], + source: "/tmp/openai/index.js", + }, + ], + }, + }); + + expect( + surface?.resolveThinkingProfile?.({ provider: "openai-codex", modelId: "gpt-5.5" }), + ).toEqual({ + levels: [{ id: "openai-codex" }], + }); + expect(loadBundledPluginPublicArtifactModuleSync).toHaveBeenCalledWith({ + dirName: "openai", + artifactBasename: "provider-policy-api.js", + }); + expect(loadPluginManifestRegistry).not.toHaveBeenCalled(); + }); + it("does not cache manifest-owned provider policy aliases across bundled metadata changes", async () => { const bundledPluginsDir = fs.mkdtempSync( path.join(os.tmpdir(), "openclaw-provider-policy-refresh-"), diff --git a/src/plugins/provider-public-artifacts.ts b/src/plugins/provider-public-artifacts.ts index 032953c5daf8..ddab7fdc65cb 100644 --- a/src/plugins/provider-public-artifacts.ts +++ b/src/plugins/provider-public-artifacts.ts @@ -91,10 +91,7 @@ function resolveBundledProviderPolicyPluginId( if (plugin.origin !== "bundled") { continue; } - const ownsProvider = plugin.providers.some( - (provider) => normalizeProviderId(provider) === normalizedProviderId, - ); - if (ownsProvider) { + if (pluginOwnsProviderPolicyRef(plugin, normalizedProviderId)) { return plugin.id; } } @@ -102,6 +99,28 @@ function resolveBundledProviderPolicyPluginId( return null; } +function pluginOwnsProviderPolicyRef( + plugin: PluginManifestRegistry["plugins"][number], + normalizedProviderId: string, +): boolean { + const ownedProviders = new Set( + plugin.providers.map((provider) => normalizeProviderId(provider)).filter(Boolean), + ); + if (ownedProviders.has(normalizedProviderId)) { + return true; + } + + for (const [rawAlias, rawTarget] of Object.entries(plugin.providerAuthAliases ?? {})) { + const alias = normalizeProviderId(rawAlias); + const target = normalizeProviderId(rawTarget); + if (alias === normalizedProviderId && ownedProviders.has(target)) { + return true; + } + } + + return false; +} + export function resolveBundledProviderPolicySurface( providerId: string, options: { manifestRegistry?: Pick } = {}, diff --git a/src/plugins/provider-runtime.test-support.ts b/src/plugins/provider-runtime.test-support.ts index 8e899ddaa965..6e2ec2f8acc0 100644 --- a/src/plugins/provider-runtime.test-support.ts +++ b/src/plugins/provider-runtime.test-support.ts @@ -17,16 +17,11 @@ export const expectedAugmentedOpenaiCodexCatalogEntries = [ { provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" }, { provider: "openai", id: "gpt-5.4-mini", name: "gpt-5.4-mini" }, { provider: "openai", id: "gpt-5.4-nano", name: "gpt-5.4-nano" }, - { provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" }, - { provider: "openai-codex", id: "gpt-5.4-pro", name: "gpt-5.4-pro" }, - { provider: "openai-codex", id: "gpt-5.4-mini", name: "gpt-5.4-mini" }, ]; export const expectedAugmentedOpenaiCodexCatalogEntriesWithGpt55 = [ { provider: "openai", id: "gpt-5.5-pro", name: "gpt-5.5-pro" }, - ...expectedAugmentedOpenaiCodexCatalogEntries.slice(0, 4), - { provider: "openai-codex", id: "gpt-5.5-pro", name: "gpt-5.5-pro" }, - ...expectedAugmentedOpenaiCodexCatalogEntries.slice(4), + ...expectedAugmentedOpenaiCodexCatalogEntries, ]; export const expectedOpenaiPluginCodexCatalogEntriesWithGpt55 = diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index d18264be26d3..efe9f4bda3ca 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -149,9 +149,6 @@ function createOpenAiCatalogProviderPlugin( { provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" }, { provider: "openai", id: "gpt-5.4-mini", name: "gpt-5.4-mini" }, { provider: "openai", id: "gpt-5.4-nano", name: "gpt-5.4-nano" }, - { provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" }, - { provider: "openai-codex", id: "gpt-5.4-pro", name: "gpt-5.4-pro" }, - { provider: "openai-codex", id: "gpt-5.4-mini", name: "gpt-5.4-mini" }, ], ...overrides, }; diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index a2be81e1ddc4..627290d5c53d 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -57,6 +57,7 @@ function createManifestProviderPlugin(params: { setup?: PluginManifestRecord["setup"]; contracts?: PluginManifestRecord["contracts"]; modelCatalog?: PluginManifestRecord["modelCatalog"]; + providerAuthAliases?: PluginManifestRecord["providerAuthAliases"]; packageManifest?: OpenClawPackageManifest; }): PluginManifestRecord { return { @@ -69,6 +70,7 @@ function createManifestProviderPlugin(params: { activation: params.activation, setup: params.setup, modelCatalog: params.modelCatalog, + providerAuthAliases: params.providerAuthAliases, packageManifest: params.packageManifest, contracts: params.contracts, skills: [], @@ -695,6 +697,22 @@ describe("resolvePluginProviders", () => { expect(getCurrentPluginMetadataSnapshotMock).not.toHaveBeenCalled(); }); + it("maps manifest provider auth aliases to the target provider owner", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "openai", + providerIds: ["openai"], + providerAuthAliases: { + "openai-codex": "openai", + }, + }), + ]); + + expectOwningPluginIds("openai", ["openai"]); + expectOwningPluginIds("openai-codex", ["openai"]); + expectModelOwningPluginIds("openai-codex/gpt-5.5", ["openai"]); + }); + it("reflects provider ownership manifest changes on the next lookup", () => { setManifestPlugins([ createManifestProviderPlugin({ diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index e151b12156ab..100fc19a659f 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -87,6 +87,17 @@ function pluginOwnsProviderRef(plugin: PluginManifestRecord, normalizedProvider: ) { return true; } + for (const [rawAlias, target] of Object.entries(plugin.providerAuthAliases ?? {})) { + const alias = normalizeProviderId(rawAlias); + const targetProvider = normalizeProviderId(target); + if ( + alias === normalizedProvider && + targetProvider && + plugin.providers.some((providerId) => normalizeProviderId(providerId) === targetProvider) + ) { + return true; + } + } for (const [rawAlias, target] of Object.entries(plugin.modelCatalog?.aliases ?? {})) { const alias = normalizeProviderId(rawAlias); const targetProvider = normalizeProviderId(target.provider); diff --git a/src/plugins/setup-descriptors.ts b/src/plugins/setup-descriptors.ts index a22f081af166..c0ef2f70f449 100644 --- a/src/plugins/setup-descriptors.ts +++ b/src/plugins/setup-descriptors.ts @@ -1,9 +1,18 @@ +import { normalizeProviderId } from "../agents/provider-id.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; -type SetupDescriptorRecord = Pick; +type SetupDescriptorRecord = Pick< + PluginManifestRecord, + "providers" | "cliBackends" | "providerAuthAliases" | "setup" +>; export function listSetupProviderIds(record: SetupDescriptorRecord): readonly string[] { - return record.setup?.providers?.map((entry) => entry.id) ?? record.providers; + const providerIds = record.setup?.providers?.map((entry) => entry.id) ?? record.providers; + const normalizedProviderIds = new Set(providerIds.map(normalizeProviderId)); + const aliases = Object.entries(record.providerAuthAliases ?? {}) + .filter(([, target]) => normalizedProviderIds.has(normalizeProviderId(target))) + .map(([alias]) => alias); + return [...providerIds, ...aliases]; } export function listSetupCliBackendIds(record: SetupDescriptorRecord): readonly string[] { diff --git a/src/plugins/setup-registry.test.ts b/src/plugins/setup-registry.test.ts index 8f91336a8474..4e093bb506f0 100644 --- a/src/plugins/setup-registry.test.ts +++ b/src/plugins/setup-registry.test.ts @@ -454,6 +454,52 @@ describe("setup-registry module loader", () => { expect(mockArg(mocks.createJiti, 0, 0)).toBe(path.join(pluginRoot, "setup-api.js")); }); + it("uses provider auth aliases to route setup provider owner lookup", () => { + const pluginRoot = makeTempDir(); + fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8"); + mocks.loadPluginManifestRegistry.mockReturnValue({ + plugins: [ + { + id: "openai", + rootDir: pluginRoot, + providerAuthAliases: { "openai-codex": "openai" }, + setup: { + providers: [{ id: "openai" }], + requiresRuntime: true, + }, + }, + ], + diagnostics: [], + }); + mocks.createJiti.mockImplementation(() => { + return () => ({ + default: { + register(api: { + registerProvider: (provider: { + id: string; + aliases: string[]; + label: string; + auth: []; + }) => void; + }) { + api.registerProvider({ + id: "openai", + aliases: ["openai-codex"], + label: "OpenAI", + auth: [], + }); + }, + }, + }); + }); + + const provider = requireRecord( + resolvePluginSetupProvider({ provider: "openai-codex", env: {} }), + ); + expect(provider.id).toBe("openai"); + expect(provider.label).toBe("OpenAI"); + }); + it("treats explicit descriptor-only setup as a runtime cutoff", () => { const pluginRoot = makeTempDir(); fs.writeFileSync( diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 4a3aad01a4d3..6f4ad8d27ddf 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -487,6 +487,7 @@ export type UnifiedModelCatalogProviderPlugin = { export type ProviderRuntimeProviderConfig = { baseUrl?: string; api?: ModelProviderConfig["api"]; + auth?: ModelProviderConfig["auth"]; models?: ModelProviderConfig["models"]; headers?: unknown; }; @@ -507,6 +508,8 @@ export type ProviderResolveDynamicModelContext = { modelId: string; modelRegistry: ModelRegistry; providerConfig?: ProviderRuntimeProviderConfig; + authProfileId?: string; + authProfileMode?: AuthProfileCredential["type"] | "aws-sdk"; }; /** @@ -695,6 +698,7 @@ export type ProviderPrepareExtraParamsContext = { workspaceDir?: string; provider: string; modelId: string; + model?: ProviderRuntimeModel; extraParams?: Record; thinkingLevel?: ThinkLevel; }; diff --git a/src/test-utils/plugin-registration.ts b/src/test-utils/plugin-registration.ts index 90546f14723f..ba37d4f14ffd 100644 --- a/src/test-utils/plugin-registration.ts +++ b/src/test-utils/plugin-registration.ts @@ -73,12 +73,17 @@ export async function registerProviderPlugins( return captured.providers; } -export function requireRegisteredProvider( - providers: T[], - providerId: string, - label = "provider", -): T { - const provider = providers.find((entry) => entry.id === providerId); +function matchesRegisteredProviderId( + entry: { id: string; hookAliases?: readonly string[] }, + id: string, +) { + return entry.id === id || entry.hookAliases?.includes(id) === true; +} + +export function requireRegisteredProvider< + T extends { id: string; hookAliases?: readonly string[] }, +>(providers: T[], providerId: string, label = "provider"): T { + const provider = providers.find((entry) => matchesRegisteredProviderId(entry, providerId)); if (!provider) { throw new Error(`${label} ${providerId} missing`); }