refactor: unify OpenAI provider identity (#88451)

* refactor: unify OpenAI provider identity

* refactor: move legacy oauth sidecar doctor helpers

* test: align OpenAI fixtures after rebase

* test: clean OpenAI provider unification

* fix: finish OpenAI provider cleanup

* fix: finish OpenAI cleanup follow-through

* fix: finish OpenAI CI cleanup
This commit is contained in:
Peter Steinberger
2026-05-31 00:29:44 +01:00
committed by GitHub
parent 8d6a6e9d03
commit 4c33aaa86c
519 changed files with 4698 additions and 5424 deletions
@@ -2871,7 +2871,7 @@ fun providerDisplayName(provider: String): String =
when (provider.trim().lowercase()) {
"openai" -> "OpenAI"
"openrouter" -> "OpenRouter"
"openai-codex", "codex" -> "Codex"
"codex" -> "Codex"
"ollama", "ollama-local" -> "Ollama Local"
else ->
provider
@@ -270,7 +270,7 @@ private fun providerPriority(provider: String): Int =
"google" -> 2
"openrouter" -> 3
"ollama", "ollama-local" -> 4
"codex", "openai-codex" -> 5
"codex" -> 5
else -> 100
}
@@ -97,7 +97,7 @@ struct MenuSessionsInjectorTests {
plan: "Pro",
error: nil),
GatewayUsageProvider(
provider: "openai-codex",
provider: "openai",
displayName: "Codex",
windows: [GatewayUsageWindow(label: "day", usedPercent: 3, resetAt: nil)],
plan: nil,
@@ -2192,7 +2192,7 @@ extension TestChatTransportState {
path: nil,
count: 1,
defaults: OpenClawChatSessionsDefaults(
modelProvider: "openai-codex",
modelProvider: "openai",
model: "gpt-5.5",
contextTokens: nil,
thinkingLevels: [
+1 -1
View File
@@ -236,7 +236,7 @@ The bundled Hermes provider detects state at `~/.hermes` by default. Use `--from
- Memory config defaults for OpenClaw file memory, plus archive or manual-review items for external memory providers such as Honcho.
- Skills that include a `SKILL.md` file under `skills/<name>/`.
- Per-skill config values from `skills.config`.
- Supported OAuth credentials from Hermes `auth.json` and OpenCode OpenAI OAuth credentials from OpenCode `auth.json` when interactive credential migration is accepted, or when `--include-secrets` is set.
- OpenCode OpenAI OAuth credentials from OpenCode `auth.json` when interactive credential migration is accepted, or when `--include-secrets` is set. Hermes `auth.json` OAuth entries are legacy state reported for manual OpenAI reauth or doctor repair.
- Supported API keys and tokens from Hermes `.env` and OpenCode `auth.json` when interactive credential migration is accepted, or when `--include-secrets` is set.
### Supported `.env` keys
+1 -1
View File
@@ -193,7 +193,7 @@ 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. Run `openclaw doctor --fix`
to migrate older `openai-codex` auth/profile state to `openai`.
to migrate older legacy OpenAI Codex prefix auth/profile state to `openai`.
Examples:
+3 -3
View File
@@ -58,7 +58,7 @@ Most confusion comes from several different surfaces sharing the Codex name:
Those surfaces are intentionally independent. Enabling the `codex` plugin makes
the native app-server features available; `openclaw doctor --fix` owns legacy
`openai-codex/*` route repair and stale session pin cleanup. Selecting
legacy Codex route repair and stale session pin cleanup. Selecting
`openai/*` for an agent model now means "run this through Codex" unless a
non-agent OpenAI API surface is being used.
@@ -97,7 +97,7 @@ This is the agent-facing decision tree:
as `openai/<model>` and set provider/model runtime policy to
`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
4. If legacy config still contains **legacy Codex model refs**, repair it to
`openai/<model>` with `openclaw doctor --fix`; doctor keeps the Codex auth
route by adding provider/model-scoped `agentRuntime.id: "codex"` where the
old model ref implied it.
@@ -202,7 +202,7 @@ keeping the public model ref as `openai/*`. Stale OpenAI runtime session pins ar
ignored by runtime selection and can be cleaned with `openclaw doctor --fix`.
If `openclaw doctor` warns that the `codex` plugin is enabled while
`openai-codex/*` remains in config, treat that as legacy route state. Run
legacy Codex model refs remain in config, treat that as legacy route state. Run
`openclaw doctor --fix` to rewrite it to `openai/*` with the Codex runtime.
## GitHub Copilot agent runtime
+2 -2
View File
@@ -30,7 +30,7 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram)
OpenAI-family routes are prefix-specific:
- `openai/<model>` uses the native Codex app-server harness for agent turns by default. This is the usual ChatGPT/Codex subscription setup.
- `openai-codex/<model>` is legacy config that doctor rewrites to `openai/<model>`.
- legacy Codex model refs are legacy config that doctor rewrites to `openai/<model>`.
- `openai/<model>` plus provider/model `agentRuntime.id: "openclaw"` uses OpenClaw's built-in runtime for explicit API-key or compatibility routes.
See [OpenAI](/providers/openai) and [Codex harness](/plugins/codex-harness). If the provider/runtime split is confusing, read [Agent runtimes](/concepts/agent-runtimes) first.
@@ -149,7 +149,7 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope
- 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` 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.
- legacy Codex GPT refs are legacy state, not a live provider route. Use `openai/gpt-5.5` on the native Codex runtime for new agent config, and run `openclaw doctor --fix` to migrate old legacy Codex model refs to canonical `openai/*` refs.
```json5
{
+1 -1
View File
@@ -414,7 +414,7 @@ That stages grounded durable candidates into the short-term dreaming store while
- short cooldowns (rate limits/timeouts/auth failures)
- longer disables (billing/credit failures)
Legacy Codex OAuth profiles whose tokens live in macOS Keychain (older onboarding before the file-based sidecar layout) are not picked up by the embedded runtime path — that path runs with `allowKeychainPrompt: false` and cannot trigger a Keychain prompt. Affected users will see a one-shot `log.warn` from the legacy sidecar loader naming `openclaw doctor --fix` and macOS Keychain (instead of the credential silently falling through to a downstream `No API key found for provider "openai-codex"`). Run `openclaw doctor --fix` once from an interactive terminal to migrate Keychain-backed legacy tokens inline into `auth-profiles.json`; after that, embedded turns (Telegram, cron, sub-agent dispatch) resolve them like any other inline OAuth profile.
Legacy Codex OAuth profiles whose tokens live in macOS Keychain (older onboarding before the file-based sidecar layout) are repaired only by doctor. Run `openclaw doctor --fix` once from an interactive terminal to migrate Keychain-backed legacy tokens inline into `auth-profiles.json`; after that, embedded turns (Telegram, cron, sub-agent dispatch) resolve them as canonical OpenAI OAuth profiles.
</Accordion>
<Accordion title="6. Hooks model validation">
+6 -6
View File
@@ -596,28 +596,28 @@ and troubleshooting see the main [FAQ](/help/faq).
<Accordion title="How does Codex auth work?">
OpenClaw supports **OpenAI Code (Codex)** via OAuth (ChatGPT sign-in). Use
`openai/gpt-5.5` for the common setup: ChatGPT/Codex subscription auth plus
native Codex app-server execution. `openai-codex/gpt-*` model refs are
native Codex app-server execution. Legacy Codex GPT 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` API-key profile.
See [Model providers](/concepts/model-providers) and [Onboarding (CLI)](/start/wizard).
</Accordion>
<Accordion title="Why does OpenClaw still mention openai-codex?">
<Accordion title="Why does OpenClaw still mention legacy OpenAI Codex prefix?">
`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
ChatGPT/Codex OAuth. You may still see legacy OpenAI Codex prefix 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`
- legacy Codex GPT-5.5 ref = legacy model route repaired by `openclaw doctor --fix`
- `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`
- legacy Codex auth profile ids = 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`. Keep the model ref as
`openai/gpt-5.5`; `openai-codex/*` model refs are legacy config that
`openai/gpt-5.5`; legacy Codex model refs are legacy config that
`openclaw doctor --fix` rewrites.
</Accordion>
+2 -2
View File
@@ -66,7 +66,7 @@ Imports require a fresh OpenClaw setup. If you already have local OpenClaw state
Skills with a `SKILL.md` file under `skills/<name>/` are copied, along with per-skill config values from `skills.config`.
</Accordion>
<Accordion title="Auth credentials">
Interactive `openclaw migrate` asks before importing auth credentials, with yes selected by default. Accepted imports include supported OAuth credentials from Hermes `auth.json`, OpenCode OpenAI OAuth credentials from OpenCode `auth.json`, OpenCode and GitHub Copilot entries from OpenCode `auth.json`, and the [supported `.env` keys](/cli/migrate#supported-env-keys). Use `--include-secrets` for non-interactive `openclaw migrate` credential import, `--no-auth-credentials` to skip it, or onboarding `--import-secrets` when importing from the onboarding wizard.
Interactive `openclaw migrate` asks before importing auth credentials, with yes selected by default. Accepted imports include OpenCode OpenAI OAuth credentials from OpenCode `auth.json`, OpenCode and GitHub Copilot entries from OpenCode `auth.json`, and the [supported `.env` keys](/cli/migrate#supported-env-keys). Hermes `auth.json` OAuth entries are legacy state and are surfaced as manual reauth/doctor work instead of imported into live auth. Use `--include-secrets` for non-interactive `openclaw migrate` credential import, `--no-auth-credentials` to skip it, or onboarding `--import-secrets` when importing from the onboarding wizard.
</Accordion>
</AccordionGroup>
@@ -137,7 +137,7 @@ If a conflict surfaces mid-apply (for example, an unexpected race on a config fi
Interactive `openclaw migrate` asks whether to import detected auth credentials, with yes selected by default.
- Accepting the prompt imports supported OAuth credentials from Hermes `auth.json`, OpenCode OpenAI OAuth credentials from OpenCode `auth.json`, OpenCode and GitHub Copilot entries from OpenCode `auth.json`, and the [supported `.env` keys](/cli/migrate#supported-env-keys).
- Accepting the prompt imports OpenCode OpenAI OAuth credentials from OpenCode `auth.json`, OpenCode and GitHub Copilot entries from OpenCode `auth.json`, and the [supported `.env` keys](/cli/migrate#supported-env-keys). Hermes `auth.json` OAuth entries are reported for manual OpenAI reauth or doctor repair.
- Use `--no-auth-credentials` or choose no at the prompt to import non-secret state only.
- Use `--include-secrets` when running unattended with `--yes`.
- Use onboarding `--import-secrets` when importing credentials from the onboarding wizard.
+12 -11
View File
@@ -17,9 +17,10 @@ selection, OpenClaw dynamic tools, approvals, media delivery, and the visible
transcript mirror.
The normal setup uses canonical OpenAI model refs such as `openai/gpt-5.5`.
Do not configure `openai-codex/gpt-*` model refs. Put OpenAI agent auth order
under `auth.order.openai`; older `openai-codex:*` profiles and
`auth.order.openai-codex` entries remain supported for existing installs.
Do not configure legacy Codex GPT refs. Put OpenAI agent auth order
under `auth.order.openai`; older legacy Codex auth profile ids and
legacy Codex auth order entries are legacy state repaired by
`openclaw doctor --fix`.
When no OpenClaw sandbox is active, OpenClaw starts Codex app-server threads
with Codex native code mode enabled while leaving code-mode-only off by default.
@@ -122,8 +123,8 @@ harness options in OpenClaw config, and use the CLI only for Codex auth:
Use `openai/gpt-*` model refs for Codex-backed OpenAI agent turns. Prefer
`auth.order.openai` for subscription-first/API-key-backup ordering. Existing
`openai-codex:*` auth profiles and `auth.order.openai-codex` remain valid, but
do not write new `openai-codex/gpt-*` model refs.
legacy Codex auth profile ids and legacy Codex auth order are doctor-only
legacy state; do not write new legacy Codex GPT refs.
Do not set `compaction.model` or `compaction.provider` on Codex-backed agents.
Codex compacts through its native app-server thread state, so OpenClaw ignores
@@ -195,7 +196,7 @@ the harness and account. If `/status` is surprising, see
Keep provider refs and runtime policy separate:
- Use `openai/gpt-*` for OpenAI agent turns through Codex.
- Do not use `openai-codex/gpt-*` in config. Run `openclaw doctor --fix` to
- Do not use legacy Codex GPT refs in config. Run `openclaw doctor --fix` to
repair legacy refs and stale session route pins.
- `agentRuntime.id: "codex"` is optional for normal OpenAI auto mode, but useful
when a deployment should fail closed if Codex is unavailable.
@@ -223,13 +224,13 @@ Common command routing:
| ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-*` plus enabled `codex` plugin | `/status` shows `Runtime: OpenAI Codex` | Recommended path |
| Fail closed if Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | Turn fails instead of embedded fallback | Use for Codex-only deployments |
| Direct OpenAI API-key traffic through OpenClaw | Provider or model `agentRuntime.id: "openclaw"` and normal OpenAI auth | `/status` shows OpenClaw runtime | Use only when OpenClaw is intentional |
| Legacy config | `openai-codex/gpt-*` | `openclaw doctor --fix` rewrites it | Do not write new config this way |
| Legacy config | legacy Codex GPT refs | `openclaw doctor --fix` rewrites it | Do not write new config this way |
| ACP/acpx Codex adapter | ACP `sessions_spawn({ runtime: "acp" })` | ACP task/session status | Separate from native Codex harness |
`agents.defaults.imageModel` follows the same prefix split. Use `openai/gpt-*`
for the normal OpenAI route and `codex/gpt-*` only when image understanding
should run through a bounded Codex app-server turn. Do not use
`openai-codex/gpt-*`; doctor rewrites that legacy prefix to `openai/gpt-*`.
legacy Codex GPT refs; doctor rewrites that legacy prefix to `openai/gpt-*`.
## Deployment patterns
@@ -445,7 +446,7 @@ Auth is selected in this order:
1. Ordered OpenAI auth profiles for the agent, preferably under
`auth.order.openai`. Run `openclaw doctor --fix` to migrate older
`openai-codex:*` profile ids and `auth.order.openai-codex`.
legacy Codex auth profile ids and legacy Codex auth order.
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
@@ -705,7 +706,7 @@ Ask affected collaborators to run this read-only command on their OpenClaw host:
```bash
(
pattern='openai/gpt-5\.[45]|agentRuntime(\.id)?|harnessRuntime|Runtime: OpenAI Codex|openai-codex|resolveSelectedOpenAIRuntimeProvider|candidateProvider[": ]+openai|status[": ]+401|Incorrect API key|No API key|api-key path|API-key path|OAuth'
pattern='openai/gpt-5\.[45]|openai[-]codex|agentRuntime(\.id)?|harnessRuntime|Runtime: OpenAI Codex|legacy OpenAI Codex prefix|resolveSelectedOpenAIRuntimeProvider|candidateProvider[": ]+openai|status[": ]+401|Incorrect API key|No API key|api-key path|API-key path|OAuth'
if ls /tmp/openclaw/openclaw-*.log >/dev/null 2>&1; then
grep -E -i -n "$pattern" /tmp/openclaw/openclaw-*.log 2>/dev/null || true
@@ -729,7 +730,7 @@ Useful excerpts usually include `openai/gpt-5.5` or `openai/gpt-5.4`,
`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`.
**Legacy Codex model refs config remains:** run `openclaw doctor --fix`.
Doctor rewrites legacy model refs to `openai/*`, removes stale session and
whole-agent runtime pins, and preserves existing auth-profile overrides.
+1 -1
View File
@@ -188,7 +188,7 @@ The bundled `codex` harness is the native Codex mode for embedded OpenClaw
agent turns. Enable the bundled `codex` plugin first, and include `codex` in
`plugins.allow` if your config uses a restrictive allowlist. Native app-server
configs should use `openai/gpt-*`; OpenAI agent turns select the Codex harness
by default. Legacy `openai-codex/*` routes should be repaired with
by default. Legacy Codex model refs routes should be repaired with
`openclaw doctor --fix`, and legacy `codex/*` model refs remain compatibility
aliases for the native harness.
+16 -16
View File
@@ -21,7 +21,7 @@ surfaces such as images, embeddings, speech, and realtime.
OpenAI API-key backup when you intentionally want API-key auth.
- **Non-agent OpenAI APIs** - direct OpenAI Platform access with usage-based
billing through `OPENAI_API_KEY` or OpenAI API-key onboarding.
- **Legacy config** - `openai-codex/*` model refs are repaired by
- **Legacy config** - legacy Codex model refs are repaired by
`openclaw doctor --fix` to `openai/*` plus the Codex runtime.
OpenAI explicitly supports subscription OAuth usage in external tools and workflows like OpenClaw.
@@ -49,7 +49,7 @@ 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 prefix | Older model/profile namespace. `openclaw doctor --fix` migrates it to `openai`. |
| legacy OpenAI Codex prefix | 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. |
@@ -58,8 +58,8 @@ The names are similar but not interchangeable:
This means a config can intentionally contain `openai/*` model refs while auth
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.
legacy Codex model refs, legacy Codex auth profile ids, and
legacy Codex auth order to the canonical OpenAI route.
<Note>
GPT-5.5 is available through both direct OpenAI Platform API-key access and
@@ -75,7 +75,7 @@ OpenClaw runtime config remains available as an opt-in compatibility route. When
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
legacy Codex model refs, `codex-cli/*`, or old runtime session pins that do not come from
explicit runtime config.
</Note>
@@ -85,7 +85,7 @@ explicit runtime config.
| ------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Chat / Responses | `openai/<model>` model provider | Yes |
| Codex subscription models | `openai/<model>` with OpenAI OAuth | Yes |
| Legacy Codex model refs | `openai-codex/<model>` or `codex-cli/<model>` | Repaired by doctor to `openai/<model>` |
| Legacy Codex model refs | legacy Codex model refs or `codex-cli/<model>` | Repaired by doctor to `openai/<model>` |
| Codex app-server harness | `openai/<model>` 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 |
| Images | `image_generate` | Yes |
@@ -185,7 +185,7 @@ Choose your preferred auth method and follow the setup steps.
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. Run `openclaw doctor --fix` to migrate older
`auth.order.openai-codex` entries.
legacy Codex auth order entries.
</Note>
### Config example
@@ -266,12 +266,12 @@ Choose your preferred auth method and follow the setup steps.
|-----------|----------------|-------|------|
| `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` OAuth profile |
| `openai-codex/gpt-5.5` | repaired by doctor | Legacy route rewritten to `openai/gpt-5.5` | Migrated OpenAI OAuth profile |
| legacy Codex GPT-5.5 ref | 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 |
<Warning>
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
legacy 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. `gpt-5.3-codex-spark` remains limited to accounts whose
Codex subscription catalog advertises that model; direct OpenAI API-key and
@@ -279,11 +279,11 @@ Choose your preferred auth method and follow the setup steps.
</Warning>
<Note>
The `openai-codex/*` model prefix is legacy config repaired by doctor. For
The legacy 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`; doctor migrates older
`auth.order.openai-codex` entries.
legacy Codex auth order entries.
</Note>
### Config example
@@ -345,7 +345,7 @@ Choose your preferred auth method and follow the setup steps.
openclaw models auth list --agent <id> --provider openai
```
If an older config still has `openai-codex/gpt-*` or a stale OpenAI runtime
If an older config still has legacy Codex GPT refs or a stale OpenAI runtime
session pin without explicit runtime config, repair it:
```bash
@@ -370,7 +370,7 @@ Choose your preferred auth method and follow the setup steps.
```
`openai/*` is the model route for OpenAI agent turns through Codex. Run
`openclaw doctor --fix` to migrate older `openai-codex` profile ids and
`openclaw doctor --fix` to migrate older legacy OpenAI Codex prefix profile ids and
order entries before relying on profile ordering.
### Status indicator
@@ -382,7 +382,7 @@ Choose your preferred auth method and follow the setup steps.
### Doctor warning
If `openai-codex/*` routes or stale OpenAI runtime pins remain in config or
If legacy Codex model refs or stale OpenAI runtime pins remain in config or
session state, `openclaw doctor --fix` rewrites them to `openai/*` with the
Codex runtime unless OpenClaw is explicitly configured.
@@ -432,7 +432,7 @@ still account-based. OpenClaw selects auth in this order:
1. Ordered OpenAI auth profiles for the agent, preferably under
`auth.order.openai`. Run `openclaw doctor --fix` to migrate older
`openai-codex:*` profiles and `auth.order.openai-codex`.
legacy Codex auth profile ids and legacy Codex auth order.
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
@@ -571,7 +571,7 @@ See [Video Generation](/tools/video-generation) for shared tool parameters, prov
## GPT-5 prompt contribution
OpenClaw adds a shared GPT-5 prompt contribution for GPT-5-family runs on OpenClaw-assembled prompt surfaces. It applies by model id, so OpenClaw/provider routes such as legacy pre-repair refs (`openai-codex/gpt-5.5`), `openrouter/openai/gpt-5.5`, `opencode/gpt-5.5`, and other compatible GPT-5 refs receive the same overlay. Older GPT-4.x models do not.
OpenClaw adds a shared GPT-5 prompt contribution for GPT-5-family runs on OpenClaw-assembled prompt surfaces. It applies by model id, so OpenClaw/provider routes such as legacy pre-repair refs (legacy Codex GPT-5.5 ref), `openrouter/openai/gpt-5.5`, `opencode/gpt-5.5`, and other compatible GPT-5 refs receive the same overlay. Older GPT-4.x models do not.
The bundled native Codex harness does not receive this OpenClaw GPT-5 overlay through Codex app-server developer instructions. Native Codex keeps Codex-owned base, model, and project-doc behavior, while OpenClaw disables Codex's built-in personality for native threads so agent workspace personality files stay authoritative. OpenClaw contributes only runtime context such as channel delivery, OpenClaw dynamic tools, ACP delegation, workspace context, and OpenClaw skills.
+1 -1
View File
@@ -37,7 +37,7 @@ For a high-level overview, see [Onboarding (CLI)](/start/wizard).
- **OpenAI Code (Codex) subscription (device pairing)**: browser pairing flow with a short-lived device code.
- Sets `agents.defaults.model` to `openai/gpt-5.5` through the Codex runtime when model is unset or already OpenAI-family.
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then stores it in auth profiles.
- Sets `agents.defaults.model` to `openai/gpt-5.5` when model is unset, `openai/*`, or `openai-codex/*`.
- Sets `agents.defaults.model` to `openai/gpt-5.5` when model is unset, `openai/*`, or legacy Codex model refs.
- **xAI (Grok) OAuth / API key**: signs in with xAI OAuth when chosen, or prompts for `XAI_API_KEY` on the API-key path, and configures xAI as a model provider.
- **OpenCode**: prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`, get it at https://opencode.ai/auth) and lets you pick the Zen or Go catalog.
- **Ollama**: offers **Cloud + Local**, **Cloud only**, or **Local only** first. `Cloud only` prompts for `OLLAMA_API_KEY` and uses `https://ollama.com`; the host-backed modes prompt for the Ollama base URL, discover available models, and auto-pull the selected local model when needed; `Cloud + Local` also checks whether that Ollama host is signed in for cloud access.
+1 -1
View File
@@ -150,7 +150,7 @@ What you set:
<Accordion title="OpenAI API key">
Uses `OPENAI_API_KEY` if present or prompts for a key, then stores the credential in auth profiles.
Sets `agents.defaults.model` to `openai/gpt-5.5` when model is unset, `openai/*`, or `openai-codex/*`.
Sets `agents.defaults.model` to `openai/gpt-5.5` when model is unset, `openai/*`, or legacy Codex model refs.
</Accordion>
<Accordion title="xAI (Grok) OAuth">
+1 -1
View File
@@ -183,7 +183,7 @@ Quick `/acp` flow from chat:
</Accordion>
<Accordion title="Model / provider / runtime selection cheat sheet">
- `openai-codex/*` - legacy Codex OAuth/subscription model route repaired by doctor.
- legacy Codex model refs - legacy Codex OAuth/subscription model route repaired by doctor.
- `openai/*` - native Codex app-server embedded runtime for OpenAI agent turns.
- `/codex ...` - native Codex conversation control.
- `/acp ...` or `runtime: "acp"` - explicit ACP/acpx control.
+1 -1
View File
@@ -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 config repaired by doctor, while the bundled
separate: legacy Codex model refs are 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.
+1 -1
View File
@@ -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 OpenAI models (`openai/*` models using `api: "openai-codex-responses"`)
- It only activates for Codex-capable OpenAI models (`openai/*` models using `api: "openai-chatgpt-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
+7 -7
View File
@@ -207,7 +207,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
sessionKey: "agent:codex:acp:test",
agent: "codex",
mode: "persistent",
model: "openai-codex/gpt-5.4",
model: "openai/gpt-5.4",
});
expect(readFirstEnsureSessionInput(ensure)).toEqual({
@@ -632,14 +632,14 @@ describe("AcpxRuntime fresh reset wrapper", () => {
sessionKey: "agent:main:acp:test",
agent: "main",
mode: "persistent",
model: "openai-codex/gpt-5.5",
model: "openai/gpt-5.5",
});
expect(readFirstEnsureSessionInput(ensure)).toEqual({
sessionKey: "agent:main:acp:test",
agent: "main",
mode: "persistent",
model: "openai-codex/gpt-5.5",
model: "openai/gpt-5.5",
});
});
@@ -678,7 +678,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
sessionKey: "agent:codex:acp:test",
agent: "codex",
mode: "persistent",
model: "openai-codex/gpt-5.5",
model: "openai/gpt-5.5",
});
expect(readFirstEnsureSessionInput(ensure)).toEqual({
@@ -710,7 +710,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
sessionKey: "agent:codex:acp:test",
agent: "codex",
mode: "persistent",
model: "openai-codex/gpt-5.4",
model: "openai/gpt-5.4",
thinking: "x-high",
});
@@ -743,7 +743,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
await runtime.setConfigOption({
handle,
key: "model",
value: "openai-codex/gpt-5.4",
value: "openai/gpt-5.4",
});
expect(setConfigOption).toHaveBeenNthCalledWith(1, {
@@ -774,7 +774,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
await runtime.setConfigOption({
handle,
key: "model",
value: "openai-codex/gpt-5.4/high",
value: "openai/gpt-5.4/high",
});
expect(setConfigOption).toHaveBeenNthCalledWith(1, {
+4 -4
View File
@@ -292,7 +292,7 @@ function createResetAwareSessionStore(
const OPENCLAW_BRIDGE_EXECUTABLE = "openclaw";
const OPENCLAW_BRIDGE_SUBCOMMAND = "acp";
const CODEX_ACP_AGENT_ID = "codex";
const CODEX_ACP_OPENCLAW_PREFIX = "openai-codex/";
const CODEX_ACP_OPENCLAW_PREFIX = "openai/";
const CODEX_ACP_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh"]);
const CODEX_ACP_THINKING_ALIASES = new Map<string, string | undefined>([
["off", undefined],
@@ -437,7 +437,7 @@ function failUnsupportedCodexAcpModel(rawModel: string, detail?: string): never
throw new AcpRuntimeError(
"ACP_INVALID_RUNTIME_OPTION",
detail ??
`Codex ACP model "${rawModel}" is not supported. Use openai-codex/<model> or <model>/<reasoning-effort>.`,
`Codex ACP model "${rawModel}" is not supported. Use openai/<model> or <model>/<reasoning-effort>.`,
);
}
@@ -498,7 +498,7 @@ function normalizeCodexAcpModelOverride(
if (parts.length > 2) {
failUnsupportedCodexAcpModel(
raw,
`Codex ACP model "${raw}" is not supported. Use openai-codex/<model> or <model>/<reasoning-effort>.`,
`Codex ACP model "${raw}" is not supported. Use openai/<model> or <model>/<reasoning-effort>.`,
);
}
const model = (parts[0] ?? "").trim();
@@ -506,7 +506,7 @@ function normalizeCodexAcpModelOverride(
if (!model) {
failUnsupportedCodexAcpModel(
raw,
`Codex ACP model "${raw}" is not supported. Use openai-codex/<model> or <model>/<reasoning-effort>.`,
`Codex ACP model "${raw}" is not supported. Use openai/<model> or <model>/<reasoning-effort>.`,
);
}
const reasoningEffort = thinkingReasoningEffort ?? modelReasoningEffort;
+2 -2
View File
@@ -1875,7 +1875,7 @@ describe("active-memory plugin", () => {
},
models: {
providers: {
"openai-codex": {
openai: {
baseUrl: "https://chatgpt.com/backend-api/codex",
models: [
{
@@ -1907,7 +1907,7 @@ describe("active-memory plugin", () => {
},
);
expect(lastEmbeddedRunParams().provider).toBe("openai-codex");
expect(lastEmbeddedRunParams().provider).toBe("openai");
expect(lastEmbeddedRunParams().model).toBe("gpt-5.5");
});
+1 -1
View File
@@ -164,7 +164,7 @@ describe("anthropic provider replay hooks", () => {
expect(
provider.normalizeConfig?.({
provider: "openai-codex",
provider: "openai",
providerConfig,
} as never),
).toBe(providerConfig);
@@ -69,7 +69,7 @@ describe("anthropic provider policy public artifact", () => {
expect(
normalizeConfig({
provider: "openai-codex",
provider: "openai",
providerConfig,
}),
).toBe(providerConfig);
+2 -2
View File
@@ -11,8 +11,8 @@ describe("Codex agent harness supports()", () => {
});
});
it("supports openai-codex as the primary OpenClaw routing id", () => {
expect(harness.supports({ provider: "openai-codex", requestedRuntime: "codex" })).toEqual({
it("supports openai as the primary OpenClaw routing id", () => {
expect(harness.supports({ provider: "openai", requestedRuntime: "codex" })).toEqual({
supported: true,
priority: 100,
});
+1 -1
View File
@@ -8,7 +8,7 @@ import type {
CodexAppServerModelListResult,
} from "./src/app-server/models.js";
const DEFAULT_CODEX_HARNESS_PROVIDER_IDS = new Set(["codex", "openai-codex", "openai"]);
const DEFAULT_CODEX_HARNESS_PROVIDER_IDS = new Set(["codex", "openai"]);
const CODEX_APP_SERVER_CONTEXT_ENGINE_HOST_CAPABILITIES = [
"bootstrap",
"assemble-before-prompt",
+1 -1
View File
@@ -127,7 +127,7 @@ describe("codex plugin", () => {
.supported,
).toBe(true);
const openAiCodex = harness.supports({
provider: "openai-codex",
provider: "openai",
modelId: "gpt-5.4",
requestedRuntime: "auto",
});
+2 -2
View File
@@ -42,7 +42,7 @@ export function buildCodexModelDefinition(model: {
return {
id,
name: model.displayName?.trim() || id,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
reasoning: model.supportedReasoningEfforts.length > 0 || shouldDefaultToReasoningModel(id),
input: model.inputModalities.includes("image") ? ["text", "image"] : ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -60,7 +60,7 @@ export function buildCodexProviderConfig(models: CodexAppServerModel[]): ModelPr
baseUrl: CODEX_BASE_URL,
apiKey: CODEX_APP_SERVER_AUTH_MARKER,
auth: "token",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
models: models.map(buildCodexModelDefinition),
};
}
+2 -2
View File
@@ -111,7 +111,7 @@ describe("codex provider", () => {
});
expectRecordFields(result.provider, {
auth: "token",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
});
expect(result.provider.models).toHaveLength(1);
expectRecordFields(result.provider.models[0], {
@@ -308,7 +308,7 @@ describe("codex provider", () => {
expectRecordFields(model, {
id: "custom-model",
provider: "codex",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api",
input: ["text"],
});
@@ -51,11 +51,11 @@ describe("Codex app-server attempt diagnostics", () => {
pluginAppCacheKey: buildCodexPluginAppCacheKey({
appServer,
agentDir: "/tmp/agent",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
accountId: "account-work",
envApiKeyFingerprint: "env-key",
}),
startupAuthProfileId: "openai-codex:work",
startupAuthProfileId: "openai:work",
appServer,
});
@@ -69,7 +69,7 @@ describe("Codex app-server attempt diagnostics", () => {
pluginConfigKeys: ["google-calendar"],
enabledPluginConfigKeys: ["google-calendar"],
appCacheKeyFingerprint: expect.stringMatching(/^sha256:/),
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
appServerTransport: "websocket",
appServerCommandSource: "config",
}),
@@ -33,7 +33,7 @@ const providerRuntimeMocks = vi.hoisted(() => ({
...params.context,
...refreshed,
type: "oauth",
provider: "openai-codex",
provider: "openai",
}
: undefined;
},
@@ -268,10 +268,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:default",
profileId: "openai:default",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -305,10 +305,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -320,7 +320,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
bridgeCodexAppServerStartOptions({
startOptions,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
...startOptions,
@@ -340,10 +340,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "token",
provider: "openai-codex",
provider: "openai",
token: "access-token",
},
});
@@ -352,7 +352,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
bridgeCodexAppServerStartOptions({
startOptions,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
...startOptions,
@@ -372,10 +372,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "api_key",
provider: "openai-codex",
provider: "openai",
key: "explicit-api-key",
},
});
@@ -384,7 +384,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
bridgeCodexAppServerStartOptions({
startOptions,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
...startOptions,
@@ -407,10 +407,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -422,7 +422,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
bridgeCodexAppServerStartOptions({
startOptions,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toBe(startOptions);
} finally {
@@ -435,34 +435,34 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "api_key",
provider: "openai-codex",
provider: "openai",
key: "first-secret-key",
},
});
const first = await resolveCodexAppServerAuthAccountCacheKey({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "api_key",
provider: "openai-codex",
provider: "openai",
key: "second-secret-key",
},
});
const second = await resolveCodexAppServerAuthAccountCacheKey({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(first).toMatch(/^openai-codex:work:api_key:sha256:[a-f0-9]{64}$/);
expect(second).toMatch(/^openai-codex:work:api_key:sha256:[a-f0-9]{64}$/);
expect(first).toMatch(/^openai:work:api_key:sha256:[a-f0-9]{64}$/);
expect(second).toMatch(/^openai:work:api_key:sha256:[a-f0-9]{64}$/);
expect(second).not.toBe(first);
expect(first).not.toContain("first-secret-key");
expect(second).not.toContain("second-secret-key");
@@ -476,27 +476,27 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "api_key",
provider: "openai-codex",
provider: "openai",
keyRef: { source: "env", provider: "default", id: "OPENAI_CODEX_TEST_KEY" },
},
});
vi.stubEnv("OPENAI_CODEX_TEST_KEY", "first-ref-secret");
const first = await resolveCodexAppServerAuthAccountCacheKey({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
vi.stubEnv("OPENAI_CODEX_TEST_KEY", "second-ref-secret");
const second = await resolveCodexAppServerAuthAccountCacheKey({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(first).toMatch(/^openai-codex:work:api_key:sha256:[a-f0-9]{64}$/);
expect(second).toMatch(/^openai-codex:work:api_key:sha256:[a-f0-9]{64}$/);
expect(first).toMatch(/^openai:work:api_key:sha256:[a-f0-9]{64}$/);
expect(second).toMatch(/^openai:work:api_key:sha256:[a-f0-9]{64}$/);
expect(second).not.toBe(first);
expect(first).not.toContain("first-ref-secret");
expect(second).not.toContain("second-ref-secret");
@@ -510,10 +510,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "token",
provider: "openai-codex",
provider: "openai",
tokenRef: { source: "env", provider: "default", id: "OPENAI_CODEX_TEST_TOKEN" },
email: "codex@example.test",
},
@@ -521,13 +521,13 @@ describe("bridgeCodexAppServerStartOptions", () => {
vi.stubEnv("OPENAI_CODEX_TEST_TOKEN", "first-ref-token");
const first = await resolveCodexAppServerAuthAccountCacheKey({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
vi.stubEnv("OPENAI_CODEX_TEST_TOKEN", "second-ref-token");
const second = await resolveCodexAppServerAuthAccountCacheKey({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(first).toMatch(/^codex@example\.test:token:sha256:[a-f0-9]{64}$/);
@@ -546,10 +546,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -561,7 +561,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(request).toHaveBeenCalledWith("account/login/start", {
@@ -629,10 +629,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:default",
profileId: "openai:default",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "default-access-token",
refresh: "default-refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -663,9 +663,9 @@ describe("bridgeCodexAppServerStartOptions", () => {
store: {
version: 1,
profiles: {
"openai-codex:default": {
"openai:default": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "",
refresh: "",
expires: Date.now() + 60_000,
@@ -687,10 +687,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:default",
profileId: "openai:default",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "ref-backed-access-token",
refresh: "ref-backed-refresh-token",
expires: Date.now() + 60_000,
@@ -732,7 +732,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
chatgptPlanType: null,
});
expect(loadAuthProfileStoreForSecretsRuntime(agentDir).profiles).not.toHaveProperty(
"openai-codex:default",
"openai:default",
);
} finally {
await fs.rm(root, { recursive: true, force: true });
@@ -792,10 +792,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:default",
profileId: "openai:default",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "default-access-token",
refresh: "default-refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -804,10 +804,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
});
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "work-access-token",
refresh: "work-refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -821,7 +821,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
config: {
auth: {
order: {
"openai-codex": ["openai-codex:work", "openai-codex:default"],
openai: ["openai:work", "openai:default"],
},
},
},
@@ -850,10 +850,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "expired-access-token",
refresh: "refresh-token",
expires: Date.now() - 60_000,
@@ -865,7 +865,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("refresh-token");
@@ -887,10 +887,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "api_key",
provider: "openai-codex",
provider: "openai",
keyRef: { source: "env", provider: "default", id: "OPENAI_CODEX_API_KEY" },
},
});
@@ -898,7 +898,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(request).toHaveBeenCalledWith("account/login/start", {
@@ -916,10 +916,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:aws",
profileId: "openai:aws",
credential: {
type: "aws-sdk",
provider: "openai-codex",
provider: "openai",
} as never,
});
@@ -927,10 +927,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:aws",
authProfileId: "openai:aws",
}),
).rejects.toThrow(
'Codex app-server auth profile "openai-codex:aws" does not contain usable credentials.',
'Codex app-server auth profile "openai:aws" does not contain usable credentials.',
);
expect(oauthMocks.refreshOpenAICodexToken).not.toHaveBeenCalled();
expect(request).not.toHaveBeenCalled();
@@ -1191,10 +1191,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "token",
provider: "openai-codex",
provider: "openai",
tokenRef: { source: "env", provider: "default", id: "OPENAI_CODEX_TOKEN" },
email: "codex@example.test",
},
@@ -1203,7 +1203,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(request).toHaveBeenCalledWith("account/login/start", {
@@ -1223,11 +1223,11 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "token",
provider: "openai-codex",
token: "sk-openai-codex-api-key-value",
provider: "openai",
token: "sk-openai-chatgpt-api-key-value",
},
});
@@ -1235,14 +1235,14 @@ describe("bridgeCodexAppServerStartOptions", () => {
applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toBeUndefined();
expect(request).toHaveBeenCalledWith("account/login/start", {
type: "chatgptAuthTokens",
accessToken: "sk-openai-codex-api-key-value",
chatgptAccountId: "openai-codex:work",
accessToken: "sk-openai-chatgpt-api-key-value",
chatgptAccountId: "openai:work",
chatgptPlanType: null,
});
} finally {
@@ -1257,10 +1257,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "api_key",
provider: "openai-codex",
provider: "openai",
key: tokenLikeKey,
},
});
@@ -1269,7 +1269,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toBeUndefined();
@@ -1288,7 +1288,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "token",
provider: "codex-cli",
@@ -1300,7 +1300,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(request).toHaveBeenCalledWith("account/login/start", {
@@ -1325,10 +1325,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "stale-access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
@@ -1340,7 +1340,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await expect(
refreshCodexAppServerAuthTokens({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
accessToken: "refreshed-access-token",
@@ -1358,7 +1358,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
const currentExpiry = Date.now() + 60_000;
oauthMocks.refreshOpenAICodexToken.mockImplementationOnce(async () => {
const persistedProfile = expectOAuthProfile(
loadAuthProfileStoreForSecretsRuntime(agentDir).profiles["openai-codex:work"],
loadAuthProfileStoreForSecretsRuntime(agentDir).profiles["openai:work"],
);
expect(persistedProfile).toMatchObject({
access: "current-access-token",
@@ -1374,10 +1374,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "current-access-token",
refresh: "refresh-token",
expires: currentExpiry,
@@ -1389,7 +1389,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await expect(
refreshCodexAppServerAuthTokens({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
accessToken: "refreshed-access-token",
@@ -1398,7 +1398,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
});
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("refresh-token");
const refreshedProfile = expectOAuthProfile(
loadAuthProfileStoreForSecretsRuntime(agentDir).profiles["openai-codex:work"],
loadAuthProfileStoreForSecretsRuntime(agentDir).profiles["openai:work"],
);
expect(refreshedProfile?.access).toBe("refreshed-access-token");
expect(refreshedProfile?.refresh).toBe("refreshed-refresh-token");
@@ -1422,10 +1422,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
});
try {
upsertAuthProfile({
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "main-current-access-token",
refresh: "main-refresh-token",
expires: Date.now() + 60_000,
@@ -1437,7 +1437,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await expect(
refreshCodexAppServerAuthTokens({
agentDir: childAgentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
accessToken: "main-refreshed-access-token",
@@ -1448,9 +1448,9 @@ describe("bridgeCodexAppServerStartOptions", () => {
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("main-refresh-token");
await expectPathMissing(childAuthPath);
const mainProfile = expectOAuthProfile(
loadAuthProfileStoreForSecretsRuntime().profiles["openai-codex:work"],
loadAuthProfileStoreForSecretsRuntime().profiles["openai:work"],
);
expect(mainProfile?.provider).toBe("openai-codex");
expect(mainProfile?.provider).toBe("openai");
expect(mainProfile?.access).toBe("main-refreshed-access-token");
expect(mainProfile?.refresh).toBe("main-refreshed-refresh-token");
} finally {
@@ -1473,10 +1473,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
});
try {
upsertAuthProfile({
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "main-current-access-token",
refresh: "main-owner-refresh-token",
expires: Date.now() + 60_000,
@@ -1490,9 +1490,9 @@ describe("bridgeCodexAppServerStartOptions", () => {
JSON.stringify({
version: 1,
profiles: {
"openai-codex:work": {
"openai:work": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "child-stale-access-token",
refresh: "child-stale-refresh-token",
expires: Date.now() - 60_000,
@@ -1506,7 +1506,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await expect(
refreshCodexAppServerAuthTokens({
agentDir: childAgentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
accessToken: "main-refreshed-access-token",
@@ -1516,13 +1516,13 @@ describe("bridgeCodexAppServerStartOptions", () => {
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("main-owner-refresh-token");
const mainProfile = expectOAuthProfile(
loadAuthProfileStoreForSecretsRuntime().profiles["openai-codex:work"],
loadAuthProfileStoreForSecretsRuntime().profiles["openai:work"],
);
expect(mainProfile?.provider).toBe("openai-codex");
expect(mainProfile?.provider).toBe("openai");
expect(mainProfile?.access).toBe("main-refreshed-access-token");
expect(mainProfile?.refresh).toBe("main-refreshed-refresh-token");
const childProfile = expectOAuthProfile(
loadAuthProfileStoreForSecretsRuntime(childAgentDir).profiles["openai-codex:work"],
loadAuthProfileStoreForSecretsRuntime(childAgentDir).profiles["openai:work"],
);
expect(childProfile?.access).toBe("child-stale-access-token");
expect(childProfile?.refresh).toBe("child-stale-refresh-token");
@@ -1542,7 +1542,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "codex-cli",
@@ -1557,7 +1557,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await expect(
refreshCodexAppServerAuthTokens({
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
).resolves.toEqual({
accessToken: "refreshed-alias-access-token",
@@ -1576,10 +1576,10 @@ describe("bridgeCodexAppServerStartOptions", () => {
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:work",
profileId: "openai:work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
@@ -1592,7 +1592,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(request).toHaveBeenCalledWith("account/login/start", {
@@ -218,7 +218,7 @@ describe("Auth profile runtime contract - Codex app-server adapter", () => {
await writeCodexAppServerBinding(sessionFile, {
threadId: "thread-auth-contract",
cwd: tmpDir,
authProfileId: "openai-codex:stale",
authProfileId: "openai:stale",
dynamicToolsFingerprint: "[]",
});
const params = createParams(sessionFile, tmpDir);
@@ -251,11 +251,11 @@ describe("maybeCompactCodexAppServerSession", () => {
seenAuthProfileId = authProfileId;
return fake.client;
});
const sessionFile = await writeTestBinding({ authProfileId: "openai-codex:work" });
const sessionFile = await writeTestBinding({ authProfileId: "openai:work" });
const result = requireCompactResult(await startCompaction(sessionFile));
expect(seenAuthProfileId).toBe("openai-codex:work");
expect(seenAuthProfileId).toBe("openai:work");
expect(result.ok).toBe(true);
});
@@ -278,7 +278,7 @@ describe("maybeCompactCodexAppServerSession", () => {
fake.request.mockRejectedValueOnce(new Error("thread not found: thread-1"));
setCodexAppServerClientFactoryForTest(async () => fake.client);
const sessionFile = await writeTestBinding({
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
model: "gpt-5.5-mini",
approvalPolicy: "on-request",
sandbox: "workspace-write",
@@ -292,7 +292,7 @@ describe("maybeCompactCodexAppServerSession", () => {
expect(fake.request).toHaveBeenCalledWith("thread/compact/start", { threadId: "thread-1" });
const preservedBinding = await readCodexAppServerBinding(sessionFile);
expect(preservedBinding?.threadId).toBe("thread-1");
expect(preservedBinding?.authProfileId).toBe("openai-codex:work");
expect(preservedBinding?.authProfileId).toBe("openai:work");
expect(preservedBinding?.model).toBe("gpt-5.5-mini");
expect(preservedBinding?.approvalPolicy).toBe("on-request");
expect(preservedBinding?.sandbox).toBe("workspace-write");
@@ -568,7 +568,7 @@ describe("maybeCompactCodexAppServerSession", () => {
await writeCodexAppServerBinding(sessionFile, {
threadId: "thread-1",
cwd: tempDir,
authProfileId: "openai-codex:binding",
authProfileId: "openai:binding",
});
const result = await maybeCompactCodexAppServerSession({
@@ -577,7 +577,7 @@ describe("maybeCompactCodexAppServerSession", () => {
sessionFile,
workspaceDir: tempDir,
trigger: "manual",
authProfileId: "openai-codex:runtime",
authProfileId: "openai:runtime",
});
expect(result).toEqual({
@@ -511,8 +511,8 @@ describe("Codex app-server dynamic tool build", () => {
const transportAuthProfileStore = {
version: 1,
profiles: {
"openai-codex:work": {
provider: "openai-codex",
"openai:work": {
provider: "openai",
type: "oauth",
access: "transport-token",
refresh: "transport-refresh",
@@ -523,8 +523,8 @@ describe("Codex app-server dynamic tool build", () => {
const toolAuthProfileStore = {
version: 1,
profiles: {
"openai-codex:work": {
provider: "openai-codex",
"openai:work": {
provider: "openai",
type: "oauth",
access: "transport-token",
refresh: "transport-refresh",
@@ -42,8 +42,8 @@ function assistantMessage(text: string, timestamp: number) {
return {
role: "assistant" as const,
content: [{ type: "text" as const, text }],
api: "openai-codex-responses",
provider: "openai-codex",
api: "openai-chatgpt-responses",
provider: "openai",
model: "gpt-5.4-codex",
usage: {
input: 0,
@@ -69,7 +69,7 @@ async function createParams(): Promise<EmbeddedRunAttemptParams> {
sessionFile,
workspaceDir: tempDir,
runId: "run-1",
provider: "openai-codex",
provider: "openai",
modelId: "gpt-5.4-codex",
model: createCodexTestModel(),
thinkLevel: "medium",
@@ -368,8 +368,8 @@ describe("CodexAppServerEventProjector", () => {
const result = projector.buildResult(buildEmptyToolTelemetry());
expect(result.lastAssistant?.provider).toBe("openai-codex");
expect(result.lastAssistant?.api).toBe("openai-codex-responses");
expect(result.lastAssistant?.provider).toBe("openai");
expect(result.lastAssistant?.api).toBe("openai-chatgpt-responses");
expect(result.lastAssistant?.model).toBe("gpt-5.5");
});
@@ -390,7 +390,7 @@ describe("CodexAppServerEventProjector", () => {
auth: {
providerForAuth: "openai",
authProfileProviderForAuth: "openai",
harnessAuthProvider: "openai-codex",
harnessAuthProvider: "openai",
forwardedAuthProfileId: "openai:work",
},
observability: {
@@ -1158,7 +1158,7 @@ describe("CodexAppServerEventProjector", () => {
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
runId: "run-1",
provider: "openai-codex",
provider: "openai",
modelId: "gpt-5.4-codex",
model: createCodexTestModel(),
thinkLevel: "medium",
@@ -1562,7 +1562,7 @@ export class CodexAppServerEventProjector {
return {
role: "assistant",
content: [{ type: "text", text }],
api: attribution.api ?? "openai-codex-responses",
api: attribution.api ?? "openai-chatgpt-responses",
provider: attribution.provider,
model: this.params.modelId,
usage,
@@ -1577,7 +1577,7 @@ export class CodexAppServerEventProjector {
return {
role: "assistant",
content: [{ type: "text", text: `${title}:\n${text}` }],
api: attribution.api ?? "openai-codex-responses",
api: attribution.api ?? "openai-chatgpt-responses",
provider: attribution.provider,
model: this.params.modelId,
usage: ZERO_USAGE,
@@ -1600,7 +1600,7 @@ export class CodexAppServerEventProjector {
input: args,
},
],
api: attribution.api ?? "openai-codex-responses",
api: attribution.api ?? "openai-chatgpt-responses",
provider: attribution.provider,
model: this.params.modelId,
usage: ZERO_USAGE,
@@ -2,7 +2,7 @@ import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness
const OPENAI_PROVIDER_ID = "openai";
const OPENAI_RESPONSES_API = "openai-responses";
const OPENAI_CODEX_RESPONSES_API = "openai-codex-responses";
const OPENAI_CODEX_RESPONSES_API = "openai-chatgpt-responses";
export type CodexLocalRuntimeAttribution = {
provider: string;
@@ -179,7 +179,7 @@ describe("Outcome/fallback runtime contract - Codex app-server adapter", () => {
text: `Codex reasoning:\n${OUTCOME_FALLBACK_RUNTIME_CONTRACT.reasoningOnlyText}`,
},
]);
expect(reasoningMessage.api).toBe("openai-codex-responses");
expect(reasoningMessage.api).toBe("openai-chatgpt-responses");
expect(reasoningMessage.provider).toBe("codex");
expect(reasoningMessage.model).toBe(OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel);
expect(reasoningMessage.usage).toStrictEqual({
@@ -245,7 +245,7 @@ describe("Outcome/fallback runtime contract - Codex app-server adapter", () => {
text: `Codex plan:\n${OUTCOME_FALLBACK_RUNTIME_CONTRACT.planningOnlyText}`,
},
]);
expect(planMessage.api).toBe("openai-codex-responses");
expect(planMessage.api).toBe("openai-chatgpt-responses");
expect(planMessage.provider).toBe("codex");
expect(planMessage.model).toBe(OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel);
expect(planMessage.usage).toStrictEqual({
@@ -140,8 +140,8 @@ export function assistantMessage(text: string, timestamp: number) {
return {
role: "assistant" as const,
content: [{ type: "text" as const, text }],
api: "openai-codex-responses",
provider: "openai-codex",
api: "openai-chatgpt-responses",
provider: "openai",
model: "gpt-5.4-codex",
usage: {
input: 0,
@@ -66,8 +66,8 @@ function assistantMessage(text: string, timestamp: number): AgentMessage {
return {
role: "assistant",
content: [{ type: "text", text }],
api: "openai-codex-responses",
provider: "openai-codex",
api: "openai-chatgpt-responses",
provider: "openai",
model: "gpt-5.4-codex",
usage: {
input: 0,
@@ -3353,7 +3353,7 @@ describe("runCodexAppServerAttempt", () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const agentDir = path.join(tempDir, "agent");
const authProfileId = "openai-codex:work";
const authProfileId = "openai:work";
const pluginConfig = {
codexPlugins: {
enabled: true,
@@ -3468,7 +3468,7 @@ describe("runCodexAppServerAttempt", () => {
profiles: {
[authProfileId]: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
@@ -3667,11 +3667,11 @@ describe("runCodexAppServerAttempt", () => {
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
);
params.authProfileId = "openai-codex:work";
params.authProfileId = "openai:work";
params.agentDir = path.join(tempDir, "agent");
const run = runCodexAppServerAttempt(params);
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai-codex:work"]), {
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai:work"]), {
interval: 1,
});
await waitForMethod("turn/start");
@@ -3679,7 +3679,7 @@ describe("runCodexAppServerAttempt", () => {
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
expect(seenAuthProfileIds).toEqual(["openai-codex:work"]);
expect(seenAuthProfileIds).toEqual(["openai:work"]);
expect(seenAgentDirs).toEqual([path.join(tempDir, "agent")]);
expect(requests.map((entry) => entry.method)).toContain("turn/start");
});
@@ -3871,7 +3871,7 @@ describe("runCodexAppServerAttempt", () => {
const workspaceDir = path.join(tempDir, "workspace");
const agentDir = path.join(tempDir, "agent");
await writeExistingBinding(sessionFile, workspaceDir, {
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
dynamicToolsFingerprint: "[]",
});
await fs.writeFile(
@@ -3921,7 +3921,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params, {
pluginConfig: { appServer: { mode: "yolo" } },
});
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai-codex:work"]), {
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai:work"]), {
interval: 1,
});
await waitForMethod("turn/start");
@@ -3930,9 +3930,9 @@ describe("runCodexAppServerAttempt", () => {
expect(requests.map((entry) => entry.method)).toContain("thread/start");
expect(requests.map((entry) => entry.method)).not.toContain("thread/resume");
expect(seenAuthProfileIds).toEqual(["openai-codex:work"]);
expect(seenAuthProfileIds).toEqual(["openai:work"]);
const savedBinding = await readCodexAppServerBinding(sessionFile);
expect(savedBinding?.authProfileId).toBe("openai-codex:work");
expect(savedBinding?.authProfileId).toBe("openai:work");
expect(savedBinding?.threadId).toBe("thread-1");
});
@@ -4246,7 +4246,7 @@ describe("runCodexAppServerAttempt", () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
await writeExistingBinding(sessionFile, workspaceDir, {
authProfileId: "openai-codex:bound",
authProfileId: "openai:bound",
dynamicToolsFingerprint: "[]",
});
const seenAuthProfileIds: Array<string | undefined> = [];
@@ -4273,7 +4273,7 @@ describe("runCodexAppServerAttempt", () => {
params.agentDir = path.join(tempDir, "agent");
const run = runCodexAppServerAttempt(params);
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai-codex:bound"]), {
await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai:bound"]), {
interval: 1,
});
await waitForMethod("turn/start");
@@ -4281,7 +4281,7 @@ describe("runCodexAppServerAttempt", () => {
await completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
expect(seenAuthProfileIds).toEqual(["openai-codex:bound"]);
expect(seenAuthProfileIds).toEqual(["openai:bound"]);
expect(seenAgentDirs).toEqual([path.join(tempDir, "agent")]);
expect(requests.map((entry) => entry.method)).toContain("turn/start");
});
@@ -17,7 +17,7 @@ describe("runCodexAppServerAttempt usage limits", () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const resetsAt = Math.ceil(Date.now() / 1000) + 120;
const authProfileId = "openai-codex:work";
const authProfileId = "openai:work";
const harnessRef: { current?: ReturnType<typeof createStartedThreadHarness> } = {};
const harness = createStartedThreadHarness(async (method) => {
if (method === "turn/start") {
@@ -40,7 +40,7 @@ describe("runCodexAppServerAttempt usage limits", () => {
profiles: {
[authProfileId]: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access",
refresh: "refresh",
expires: Date.now() + 60_000,
@@ -58,7 +58,7 @@ describe("runCodexAppServerAttempt usage limits", () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const resetsAt = Math.ceil(Date.now() / 1000) + 120;
const authProfileId = "openai-codex:work";
const authProfileId = "openai:work";
rememberCodexRateLimits({
rateLimits: {
limitId: "codex",
@@ -87,7 +87,7 @@ describe("runCodexAppServerAttempt usage limits", () => {
profiles: {
[authProfileId]: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access",
refresh: "refresh",
expires: Date.now() + 60_000,
@@ -19,7 +19,7 @@ const nativeAuthLookup: Pick<CodexAppServerAuthProfileLookup, "authProfileStore"
profiles: {
work: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
@@ -237,7 +237,7 @@ describe("codex app-server session binding", () => {
{
threadId: "thread-123",
cwd: tempDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
model: "gpt-5.4-mini",
modelProvider: "openai",
},
@@ -245,7 +245,7 @@ describe("codex app-server session binding", () => {
authProfileStore: {
version: 1,
profiles: {
"openai-codex:work": {
"openai:work": {
type: "api_key",
provider: "openai",
key: "sk-test",
@@ -259,7 +259,7 @@ describe("codex app-server session binding", () => {
authProfileStore: {
version: 1,
profiles: {
"openai-codex:work": {
"openai:work": {
type: "api_key",
provider: "openai",
key: "sk-test",
@@ -283,7 +283,7 @@ describe("codex app-server session binding", () => {
{
threadId: "thread-123",
cwd: tempDir,
authProfileId: "openai-codex:default",
authProfileId: "openai:default",
model: "gpt-5.4-mini",
modelProvider: "openai",
},
@@ -294,7 +294,7 @@ describe("codex app-server session binding", () => {
const binding = await readCodexAppServerBinding(sessionFile, { agentDir });
expect(raw).not.toContain('"modelProvider": "openai"');
expect(binding?.authProfileId).toBe("openai-codex:default");
expect(binding?.authProfileId).toBe("openai:default");
expect(binding?.modelProvider).toBeUndefined();
});
@@ -205,22 +205,22 @@ describe("shared Codex app-server client", () => {
const listPromise = listCodexAppServerModels({
timeoutMs: 1000,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)");
await sendEmptyModelList(harness);
await expect(listPromise).resolves.toEqual({ models: [] });
const bridgeCall = bridgeStartOptionsCall();
expect(bridgeCall?.authProfileId).toBe("openai-codex:work");
expect(bridgeCall?.authProfileId).toBe("openai:work");
const applyCall = applyAuthProfileCall();
expect(applyCall?.authProfileId).toBe("openai-codex:work");
expect(applyCall?.authProfileId).toBe("openai:work");
});
it("skips target auth resolution when native source auth is requested", async () => {
const harness = createClientHarness();
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
const config = { auth: { order: { "openai-codex": ["openai-codex:target"] } } };
const config = { auth: { order: { openai: ["openai:target"] } } };
const clientPromise = getSharedCodexAppServerClient({
timeoutMs: 1000,
@@ -245,8 +245,8 @@ describe("shared Codex app-server client", () => {
it("resolves the configured implicit auth profile before sharing a client", async () => {
const harness = createClientHarness();
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } };
mocks.resolveCodexAppServerAuthProfileIdForAgent.mockReturnValue("openai-codex:work");
const config = { auth: { order: { openai: ["openai:work"] } } };
mocks.resolveCodexAppServerAuthProfileIdForAgent.mockReturnValue("openai:work");
const listPromise = listCodexAppServerModels({
timeoutMs: 1000,
@@ -263,10 +263,10 @@ describe("shared Codex app-server client", () => {
config,
});
const bridgeCall = bridgeStartOptionsCall();
expect(bridgeCall?.authProfileId).toBe("openai-codex:work");
expect(bridgeCall?.authProfileId).toBe("openai:work");
expect(bridgeCall?.config).toBe(config);
const applyCall = applyAuthProfileCall();
expect(applyCall?.authProfileId).toBe("openai-codex:work");
expect(applyCall?.authProfileId).toBe("openai:work");
expect(applyCall?.config).toBe(config);
});
@@ -276,7 +276,7 @@ describe("shared Codex app-server client", () => {
const listPromise = listCodexAppServerModels({
timeoutMs: 1000,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
agentDir: "/tmp/openclaw-agent-nova",
});
await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)");
@@ -285,10 +285,10 @@ describe("shared Codex app-server client", () => {
await expect(listPromise).resolves.toEqual({ models: [] });
const bridgeCall = bridgeStartOptionsCall();
expect(bridgeCall?.agentDir).toBe("/tmp/openclaw-agent-nova");
expect(bridgeCall?.authProfileId).toBe("openai-codex:work");
expect(bridgeCall?.authProfileId).toBe("openai:work");
const applyCall = applyAuthProfileCall();
expect(applyCall?.agentDir).toBe("/tmp/openclaw-agent-nova");
expect(applyCall?.authProfileId).toBe("openai-codex:work");
expect(applyCall?.authProfileId).toBe("openai:work");
});
it("migrates legacy singleton global state into the keyed registry", async () => {
@@ -308,7 +308,7 @@ function sideParams(overrides: Partial<Parameters<typeof runCodexAppServerSideQu
sessionId: "session-1",
sessionFile: "/tmp/session-1.jsonl",
workspaceDir: "/tmp/workspace",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
authProfileIdSource: "user",
...overrides,
} satisfies Parameters<typeof runCodexAppServerSideQuestion>[0];
@@ -342,7 +342,7 @@ describe("runCodexAppServerSideQuestion", () => {
threadId: "parent-thread",
sessionFile: "/tmp/session-1.jsonl",
cwd: "/tmp/workspace",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
model: "gpt-5.5",
approvalPolicy: "on-request",
sandbox: "workspace-write",
@@ -737,7 +737,7 @@ describe("runCodexAppServerSideQuestion", () => {
threadId: "parent-thread",
sessionFile: "/tmp/session-1.jsonl",
cwd: "/tmp/workspace",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
model: "gpt-5.5",
approvalPolicy: "never",
sandbox: "workspace-write",
@@ -1233,7 +1233,7 @@ describe("runCodexAppServerSideQuestion", () => {
expect(refreshCodexAppServerAuthTokensMock).toHaveBeenCalledWith({
agentDir: "/tmp/agent",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
config: {},
});
});
@@ -4,12 +4,12 @@ import type { Api, Model } from "openclaw/plugin-sdk/llm";
import { vi } from "vitest";
import { CodexAppServerClient } from "./client.js";
export function createCodexTestModel(provider = "openai-codex", input = ["text"]): Model {
export function createCodexTestModel(provider = "openai", input = ["text"]): Model {
return {
id: "gpt-5.4-codex",
name: "gpt-5.4-codex",
provider,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
input,
reasoning: true,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -1336,7 +1336,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
cwd: workspaceDir,
model: "gpt-5.4-codex",
modelProvider: "openai",
authProfileId: "openai-codex:bound",
authProfileId: "openai:bound",
});
const params = createParams(sessionFile, workspaceDir);
delete params.authProfileId;
@@ -1370,6 +1370,6 @@ describe("Codex app-server thread lifecycle bindings", () => {
},
});
expect(binding.authProfileId).toBe("openai-codex:bound");
expect(binding.authProfileId).toBe("openai:bound");
});
});
@@ -23,7 +23,7 @@ function createAttemptParams(params: {
const authProfileProviders =
params.authProfileProviders ??
(params.authProfileId
? { [params.authProfileId]: params.authProfileProvider ?? "openai-codex" }
? { [params.authProfileId]: params.authProfileProvider ?? "openai" }
: {});
return {
provider: params.provider,
@@ -552,7 +552,7 @@ describe("Codex app-server turn params", () => {
});
describe("Codex app-server model provider selection", () => {
it.each(["openai", "openai-codex"])(
it.each(["openai", "openai"])(
"omits public %s modelProvider when forwarding native Codex auth on thread/start",
(provider) => {
const request = buildThreadStartParams(
@@ -573,7 +573,7 @@ describe("Codex app-server model provider selection", () => {
const request = buildThreadResumeParams(
createAttemptParams({
provider: "openai",
authProfileProviders: { bound: "openai-codex" },
authProfileProviders: { bound: "openai" },
}),
{
threadId: "thread-1",
@@ -590,7 +590,7 @@ describe("Codex app-server model provider selection", () => {
const request = buildThreadStartParams(
createAttemptParams({
provider: "openai",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
authProfileProvider: "openai",
}),
{
@@ -1223,16 +1223,13 @@ export function resolveCodexAppServerModelProvider(params: {
// native provider/auth selection instead of forcing the legacy OpenAI path.
return undefined;
}
if (
isCodexAppServerNativeAuthProfile(params) &&
(normalizedLower === "openai" || normalizedLower === "openai-codex")
) {
if (isCodexAppServerNativeAuthProfile(params) && normalizedLower === "openai") {
// When OpenClaw is forwarding ChatGPT/Codex OAuth, `openai` is Codex's
// native provider id, not a public OpenAI API-key choice. Omit the override
// so app-server keeps its configured provider/auth pair for this session.
return undefined;
}
return normalizedLower === "openai-codex" ? "openai" : normalized;
return normalizedLower === "openai" ? "openai" : normalized;
}
// Modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark) use the
@@ -114,8 +114,8 @@ describe("Codex trajectory recorder", () => {
const parsed = JSON.parse(
fs.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8"),
);
expect(parsed.provider).toBe("openai-codex");
expect(parsed.modelApi).toBe("openai-codex-responses");
expect(parsed.provider).toBe("openai");
expect(parsed.modelApi).toBe("openai-chatgpt-responses");
expect(parsed.modelId).toBe("gpt-5.5");
});
+30 -31
View File
@@ -735,7 +735,7 @@ describe("codex command", () => {
});
it("shows model ids from Codex app-server", async () => {
const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } };
const config = { auth: { order: { openai: ["openai:work"] } } };
const listCodexAppServerModels = vi.fn(async (_options?: { config?: unknown }) => ({
models: [
{
@@ -825,7 +825,7 @@ describe("codex command", () => {
});
it("reports status unavailable when every Codex probe fails", async () => {
const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } };
const config = { auth: { order: { openai: ["openai:work"] } } };
const offline = { ok: false as const, error: "offline" };
const deps = createDeps({
readCodexStatusProbes: vi.fn(async () => ({
@@ -1277,7 +1277,7 @@ describe("codex command", () => {
profiles: {
"openai:personal-email@gmail.com": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: now + 60 * 60 * 1000,
@@ -1349,7 +1349,7 @@ describe("codex command", () => {
profiles: {
"openai:personal-email@gmail.com": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: now + 60 * 60 * 1000,
@@ -1402,12 +1402,11 @@ describe("codex command", () => {
expect(result.text).not.toContain("subscription unavailable");
});
it("shows Codex auth order before OpenAI fallback order", async () => {
it("shows OpenAI subscription auth before API-key fallback order", async () => {
const config = {
auth: {
order: {
openai: ["openai:api-key"],
"openai-codex": ["openai-codex:personal-email@gmail.com"],
openai: ["openai:personal-email@gmail.com", "openai:api-key"],
},
},
};
@@ -1421,9 +1420,9 @@ describe("codex command", () => {
provider: "openai",
key: "sk-test",
},
"openai-codex:personal-email@gmail.com": {
"openai:personal-email@gmail.com": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: now + 60 * 60 * 1000,
@@ -1431,7 +1430,7 @@ describe("codex command", () => {
},
},
lastGood: {
"openai-codex": "openai-codex:personal-email@gmail.com",
openai: "openai:personal-email@gmail.com",
},
},
config,
@@ -1463,7 +1462,7 @@ describe("codex command", () => {
expect(result.text).toContain(
"\n 1. personal-email@gmail.com ChatGPT subscription — active now",
);
expect(result.text).not.toContain("api-key");
expect(result.text).toContain("\n 2. api-key API key — available if needed");
});
it("explains when an API-key backup is active because the subscription is paused", async () => {
@@ -1477,7 +1476,7 @@ describe("codex command", () => {
profiles: {
"openai:personal-email@gmail.com": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: now + 60 * 60 * 1000,
@@ -1490,7 +1489,7 @@ describe("codex command", () => {
},
"openai:work-email@gmail.com": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "work-access-token",
refresh: "work-refresh-token",
expires: now + 60 * 60 * 1000,
@@ -1587,7 +1586,7 @@ describe("codex command", () => {
profiles: {
"openai:personal-email@gmail.com": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: now + 60 * 60 * 1000,
@@ -1646,10 +1645,10 @@ describe("codex command", () => {
expect(result.text).toContain("Now using: api-key-backup");
expect(result.text).toContain("subscription rate-limited");
expect(result.text).toContain(
"\n 1. api-key-backup API key — active now \u00b7 billed per token",
"\n 1. personal-email@gmail.com ChatGPT subscription — rate-limited",
);
expect(result.text).toContain(
"\n 2. personal-email@gmail.com ChatGPT subscription — rate-limited",
"\n 2. api-key-backup API key — active now \u00b7 billed per token",
);
expect(result.text).not.toContain(
"personal-email@gmail.com ChatGPT subscription — active now",
@@ -1663,17 +1662,17 @@ describe("codex command", () => {
{
version: 1,
profiles: {
"openai-codex:default": {
"openai:default": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "stale-access-token",
refresh: "stale-refresh-token",
expires: now + 2 * 24 * 60 * 60 * 1000,
email: "previous@example.com",
},
"openai-codex:fresh-email@example.com": {
"openai:fresh-email@example.com": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "fresh-access-token",
refresh: "fresh-refresh-token",
expires: now + 9 * 24 * 60 * 60 * 1000,
@@ -1681,10 +1680,10 @@ describe("codex command", () => {
},
},
order: {
"openai-codex": ["openai-codex:fresh-email@example.com", "openai-codex:default"],
openai: ["openai:fresh-email@example.com", "openai:default"],
},
lastGood: {
"openai-codex": "openai-codex:default",
openai: "openai:default",
},
},
config,
@@ -1717,7 +1716,7 @@ describe("codex command", () => {
"\n 2. previous@example.com ChatGPT subscription — available if needed",
);
expect(result.text).not.toContain("previous@example.com ChatGPT subscription — active now");
expect(result.text).not.toContain("openai-codex:");
expect(result.text).not.toContain("openai:");
expect(safeCodexControlRequest).toHaveBeenCalledTimes(2);
});
@@ -1779,26 +1778,26 @@ describe("codex command", () => {
{
version: 1,
profiles: {
"openai-codex:fresh@example.com": {
"openai:fresh@example.com": {
type: "token",
provider: "openai-codex",
provider: "openai",
token: "fresh-token",
expires: now - 1000,
email: "fresh@example.com",
},
"openai-codex:stale@example.com": {
"openai:stale@example.com": {
type: "token",
provider: "openai-codex",
provider: "openai",
token: "stale-token",
expires: now - 2000,
email: "stale@example.com",
},
},
order: {
"openai-codex": ["openai-codex:fresh@example.com", "openai-codex:stale@example.com"],
openai: ["openai:fresh@example.com", "openai:stale@example.com"],
},
lastGood: {
"openai-codex": "openai-codex:stale@example.com",
openai: "openai:stale@example.com",
},
},
config,
@@ -3319,7 +3318,7 @@ describe("codex command", () => {
schemaVersion: 1,
threadId: "thread-123",
cwd: "/repo",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
modelProvider: "openai",
}),
);
@@ -3371,7 +3370,7 @@ describe("codex command", () => {
threadId: "thread-123",
model: "gpt-5.4",
modelProvider: "openai",
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
});
expect(requestConversationBinding).toHaveBeenCalledWith({
summary: "Codex app-server thread thread-123 in /repo",
@@ -127,20 +127,20 @@ describe("codex conversation binding", () => {
it("uses the default Codex auth profile and omits the public OpenAI provider for new binds", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const config = {
auth: { order: { "openai-codex": ["openai-codex:default"] } },
auth: { order: { openai: ["openai:default"] } },
};
const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
"openai-codex:default": {
"openai:default": {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
},
},
});
agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue(["openai-codex:default"]);
agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue(["openai:default"]);
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
requests.push({ method, params: requestParams });
@@ -164,18 +164,18 @@ describe("codex conversation binding", () => {
provider?: unknown;
};
expect(authOrderParams?.cfg).toBe(config);
expect(authOrderParams?.provider).toBe("openai-codex");
expect(authOrderParams?.provider).toBe("openai");
const sharedClientParams = mockCallArg(sharedClientMocks.getSharedCodexAppServerClient) as {
authProfileId?: unknown;
};
expect(sharedClientParams?.authProfileId).toBe("openai-codex:default");
expect(sharedClientParams?.authProfileId).toBe("openai:default");
expect(requests).toHaveLength(1);
expect(requests[0]?.method).toBe("thread/start");
expect(requests[0]?.params.model).toBe("gpt-5.4-mini");
expect(requests[0]?.params.personality).toBe("none");
expect(requests[0]?.params).not.toHaveProperty("modelProvider");
await expect(fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8")).resolves.toContain(
'"authProfileId": "openai-codex:default"',
'"authProfileId": "openai:default"',
);
});
@@ -186,7 +186,7 @@ describe("codex conversation binding", () => {
profiles: {
work: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
@@ -622,7 +622,7 @@ describe("codex conversation binding", () => {
profiles: {
work: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
},
},
@@ -1045,7 +1045,7 @@ describe("codex conversation binding", () => {
schemaVersion: 1,
threadId: "thread-1",
cwd: tempDir,
authProfileId: "openai-codex:work",
authProfileId: "openai:work",
}),
);
const unhandledRejections: unknown[] = [];
+2 -5
View File
@@ -714,13 +714,10 @@ function resolveThreadRequestModelProvider(params: {
if (!modelProvider || modelProvider.toLowerCase() === "codex") {
return undefined;
}
if (
isCodexAppServerNativeAuthProfile(params) &&
(modelProvider.toLowerCase() === "openai" || modelProvider.toLowerCase() === "openai-codex")
) {
if (isCodexAppServerNativeAuthProfile(params) && modelProvider.toLowerCase() === "openai") {
return undefined;
}
return modelProvider.toLowerCase() === "openai-codex" ? "openai" : modelProvider;
return modelProvider.toLowerCase() === "openai" ? "openai" : modelProvider;
}
function buildAgentLookup(params: {
@@ -71,7 +71,7 @@ describe("codex conversation controls", () => {
profileId: "work",
credential: {
type: "oauth",
provider: "openai-codex",
provider: "openai",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
+2 -2
View File
@@ -193,7 +193,7 @@ describe("discord config schema", () => {
const cfg = expectValidDiscordConfig({
voice: {
mode: "agent-proxy",
model: "openai-codex/gpt-5.5",
model: "openai/gpt-5.5",
followUsersEnabled: true,
followUsers: ["58398277829140480"],
realtime: {
@@ -219,7 +219,7 @@ describe("discord config schema", () => {
});
expect(cfg.voice?.mode).toBe("agent-proxy");
expect(cfg.voice?.model).toBe("openai-codex/gpt-5.5");
expect(cfg.voice?.model).toBe("openai/gpt-5.5");
expect(cfg.voice?.followUsersEnabled).toBe(true);
expect(cfg.voice?.followUsers).toEqual(["58398277829140480"]);
expect(cfg.voice?.realtime?.provider).toBe("openai");
+1 -1
View File
@@ -195,7 +195,7 @@ export const discordChannelConfigUiHints = {
},
"voice.model": {
label: "Discord Voice Model",
help: "Optional LLM model override for Discord voice channel responses and realtime agent consults (for example openai-codex/gpt-5.5). Leave unset to inherit the routed agent model.",
help: "Optional LLM model override for Discord voice channel responses and realtime agent consults (for example openai/gpt-5.5). Leave unset to inherit the routed agent model.",
},
"voice.mode": {
label: "Discord Voice Mode",
@@ -933,7 +933,7 @@ describe("Discord model picker interactions", () => {
it("opens the first visible provider when the current model provider is filtered out", async () => {
const context = createModelPickerContext();
const pickerData = createModelsProviderData({
"openai-codex": ["gpt-5.5-codex"],
openai: ["gpt-5.5-codex"],
vllm: ["qwen3-local"],
});
pickerData.resolvedDefault = {
@@ -950,7 +950,7 @@ describe("Discord model picker interactions", () => {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
models: {
"openai-codex/*": {},
"openai/*": {},
"vllm/*": {},
},
},
@@ -970,7 +970,7 @@ describe("Discord model picker interactions", () => {
expect(loadSpy).toHaveBeenCalledWith(cfg, "main");
const payload = JSON.stringify(firstMockArg(interaction.reply, "interaction.reply"));
expect(payload).toContain("openai-codex");
expect(payload).toContain("openai");
expect(payload).toContain("gpt-5.5-codex");
expect(payload).not.toContain("Provider not found");
});
@@ -489,7 +489,7 @@ describe("Discord native plugin command dispatch", () => {
const interaction = createInteraction();
runtimeModuleMocks.getSessionEntry.mockReturnValue({
sessionId: "discord-session",
authProfileOverride: "openai-codex:owner@example.com",
authProfileOverride: "openai:owner@example.com",
updatedAt: Date.now(),
});
@@ -516,7 +516,7 @@ describe("Discord native plugin command dispatch", () => {
mock: executeSpy,
commandName: "pair",
expected: {
authProfileId: "openai-codex:owner@example.com",
authProfileId: "openai:owner@example.com",
},
});
});
@@ -541,7 +541,7 @@ describe("Discord native plugin command dispatch", () => {
}));
runtimeModuleMocks.getSessionEntry.mockReturnValue({
sessionId: "codex-session",
authProfileOverride: "openai-codex:owner@example.com",
authProfileOverride: "openai:owner@example.com",
updatedAt: Date.now(),
});
@@ -570,7 +570,7 @@ describe("Discord native plugin command dispatch", () => {
expected: {
agentId: "codex",
sessionKey: pluginSessionKey,
authProfileId: "openai-codex:owner@example.com",
authProfileId: "openai:owner@example.com",
},
});
expect(runtimeModuleMocks.getSessionEntry).toHaveBeenCalledWith({
@@ -137,7 +137,7 @@ function installProviderThinkingRegistryForTest(): void {
provider: {
id: "discord-test-thinking",
label: "Discord Test Thinking",
aliases: ["anthropic", "openai-codex"],
aliases: ["anthropic", "openai"],
auth: [],
isBinaryThinking: (context) =>
providerThinkingMocks.resolveProviderBinaryThinking({
@@ -181,7 +181,7 @@ describe("discord native /think autocomplete", () => {
providerThinkingMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined);
providerThinkingMocks.resolveProviderThinkingProfile.mockReturnValue(undefined);
providerThinkingMocks.resolveProviderXHighThinking.mockImplementation(({ provider, context }) =>
provider === "openai-codex" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId)
provider === "openai" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId)
? true
: undefined,
);
@@ -212,7 +212,7 @@ describe("discord native /think autocomplete", () => {
providerThinkingMocks.resolveProviderThinkingProfile.mockReturnValue(undefined);
providerThinkingMocks.resolveProviderXHighThinking.mockReset();
providerThinkingMocks.resolveProviderXHighThinking.mockImplementation(({ provider, context }) =>
provider === "openai-codex" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId)
provider === "openai" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId)
? true
: undefined,
);
@@ -223,7 +223,7 @@ describe("discord native /think autocomplete", () => {
JSON.stringify({
[SESSION_KEY]: {
updatedAt: Date.now(),
providerOverride: "openai-codex",
providerOverride: "openai",
modelOverride: "gpt-5.4",
},
}),
@@ -292,7 +292,7 @@ describe("discord native /think autocomplete", () => {
threadBindings: createNoopThreadBindingManager("default"),
});
expect(context).toEqual({
provider: "openai-codex",
provider: "openai",
model: "gpt-5.4",
});
@@ -2212,7 +2212,7 @@ describe("DiscordVoiceManager", () => {
groupPolicy: "open",
voice: {
enabled: true,
model: "openai-codex/gpt-5.5",
model: "openai/gpt-5.5",
realtime: {
provider: "openai",
model: "gpt-realtime-2",
@@ -2297,7 +2297,7 @@ describe("DiscordVoiceManager", () => {
);
const commandArgs = lastAgentCommandArgs();
expect(commandArgs.model).toBe("openai-codex/gpt-5.5");
expect(commandArgs.model).toBe("openai/gpt-5.5");
expect(commandArgs.messageProvider).toBe("discord-voice");
expect(commandArgs.toolsAllow).toBeUndefined();
expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledTimes(1);
@@ -4465,7 +4465,7 @@ describe("DiscordVoiceManager", () => {
voice: {
enabled: true,
mode: "bidi",
model: "openai-codex/gpt-5.5",
model: "openai/gpt-5.5",
realtime: {
provider: "openai",
model: "gpt-realtime-2",
+1 -1
View File
@@ -22,7 +22,7 @@
"items": {
"type": "string"
},
"description": "Allowlist of provider/model keys like openai-codex/gpt-5.2."
"description": "Allowlist of provider/model keys like openai/gpt-5.2."
},
"maxTokens": {
"type": "integer",
+112 -145
View File
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import { loadAuthProfileStoreWithoutExternalProfiles } from "openclaw/plugin-sdk/agent-runtime";
import {
createMigrationItem,
createMigrationManualItem,
markMigrationItemConflict,
markMigrationItemError,
markMigrationItemSkipped,
@@ -37,8 +38,8 @@ import {
import type { HermesSource } from "./source.js";
import type { PlannedTargets } from "./targets.js";
const HERMES_OPENAI_CODEX_SOURCE_PROVIDER_ID = "openai-codex";
const OPENAI_PROVIDER_ID = "openai";
const LEGACY_OPENAI_PROVIDER_ID = ["openai", "codex"].join("-");
const OPENAI_DEFAULT_MODEL = "openai/gpt-5.5";
const HERMES_AUTH_DISPLAY_NAME = "Hermes import";
@@ -51,7 +52,7 @@ type HermesCodexAuthCandidate = {
access: string;
accountId?: string;
refresh: string;
sourceKind: "hermes-auth-json" | "opencode-auth-json";
sourceKind: "opencode-auth-json";
sourceCredentialIndex?: number;
sourceLabel: string;
sourcePath: string;
@@ -65,14 +66,6 @@ type HermesCodexAuthProfile = {
sourceProfileId: string;
};
function readTimestamp(value: unknown): number | undefined {
if (typeof value !== "string" || !value.trim()) {
return undefined;
}
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function sourceCredentialFingerprint(candidate: HermesCodexAuthCandidate): string {
const hash = createHash("sha256");
for (const part of [
@@ -87,86 +80,6 @@ function sourceCredentialFingerprint(candidate: HermesCodexAuthCandidate): strin
return hash.digest("hex");
}
function readProviderTokens(
auth: Record<string, unknown>,
sourcePath: string,
): HermesCodexAuthCandidate | undefined {
const providers = isRecord(auth.providers) ? auth.providers : {};
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);
const refresh = readString(tokens?.refresh_token);
if (!access || !refresh) {
return undefined;
}
return {
access,
refresh,
sourceKind: "hermes-auth-json",
sourceLabel: "Hermes active OpenAI Codex provider",
sourcePath,
updatedAt: readTimestamp(provider?.last_refresh),
};
}
function readPoolTokens(
auth: Record<string, unknown>,
sourcePath: string,
): HermesCodexAuthCandidate[] {
const pool = isRecord(auth.credential_pool) ? auth.credential_pool : {};
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) {
if (!isRecord(entry)) {
continue;
}
const access = readString(entry.access_token);
const refresh = readString(entry.refresh_token);
if (!access || !refresh) {
continue;
}
const label = readString(entry.label) ?? "Hermes OpenAI Codex credential pool";
candidates.push({
access,
refresh,
sourceKind: "hermes-auth-json",
sourceLabel: label,
sourcePath,
updatedAt: readTimestamp(entry.last_refresh) ?? readTimestamp(entry.last_status_at),
});
}
return candidates;
}
async function readHermesCodexAuthCandidates(
authPath: string | undefined,
): Promise<HermesCodexAuthCandidate[]> {
const raw = await readText(authPath);
if (!raw || !authPath) {
return [];
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return [];
}
if (!isRecord(parsed)) {
return [];
}
const candidates = [readProviderTokens(parsed, authPath), ...readPoolTokens(parsed, authPath)]
.filter((candidate): candidate is HermesCodexAuthCandidate => candidate !== undefined)
.toSorted((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
candidates.forEach((candidate, index) => {
candidate.sourceCredentialIndex = index;
});
return candidates;
}
async function readOpenCodeOpenAICandidates(
authPath: string | undefined,
): Promise<HermesCodexAuthCandidate[]> {
@@ -203,6 +116,48 @@ async function readOpenCodeOpenAICandidates(
];
}
async function hasLegacyHermesAuthJson(authPath: string | undefined): Promise<boolean> {
const raw = await readText(authPath);
if (!raw) {
return false;
}
try {
const parsed: unknown = JSON.parse(raw);
return (
isRecord(parsed) &&
(hasLegacyOpenAIOAuthTokenFields(parsed.providers, "providers") ||
hasLegacyOpenAIOAuthTokenFields(parsed.credential_pool, "credential_pool") ||
hasLegacyOpenAIOAuthTokenFields(parsed.tokens, "tokens"))
);
} catch {
return false;
}
}
function hasLegacyOpenAIOAuthTokenFields(value: unknown, keyHint = ""): boolean {
if (Array.isArray(value)) {
return value.some((entry) => hasLegacyOpenAIOAuthTokenFields(entry, keyHint));
}
if (!isRecord(value)) {
return false;
}
const provider = readString(value.provider)?.toLowerCase();
const normalizedKeyHint = keyHint.toLowerCase();
const isOpenAIRecord =
normalizedKeyHint.includes("openai") ||
provider === OPENAI_PROVIDER_ID ||
provider === LEGACY_OPENAI_PROVIDER_ID;
const hasTokenPair =
(readString(value.access) && readString(value.refresh)) ||
(readString(value.access_token) && readString(value.refresh_token));
if (isOpenAIRecord && hasTokenPair) {
return true;
}
return Object.entries(value).some(([key, entry]) =>
hasLegacyOpenAIOAuthTokenFields(entry, keyHint ? `${keyHint}.${key}` : key),
);
}
function buildAuthResult(
candidate: HermesCodexAuthCandidate,
fallbackProfileName = "hermes-import",
@@ -282,10 +237,9 @@ function authProfileDedupeKey(profile: HermesCodexAuthProfile): string {
async function readCodexAuthProfilesFromSource(
source: HermesSource,
): Promise<HermesCodexAuthProfile[]> {
const candidates = [
...(await readHermesCodexAuthCandidates(source.authPath)),
...(await readOpenCodeOpenAICandidates(source.opencodeAuthPath)),
].toSorted((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
const candidates = (await readOpenCodeOpenAICandidates(source.opencodeAuthPath)).toSorted(
(left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0),
);
const profiles: HermesCodexAuthProfile[] = [];
const seen = new Set<string>();
for (const [index, candidate] of candidates.entries()) {
@@ -323,11 +277,7 @@ async function readCodexAuthProfilesFromPath(params: {
...(params.sourcePath ? { opencodeAuthPath: params.sourcePath } : {}),
});
}
return await readCodexAuthProfilesFromSource({
root: "",
archivePaths: [],
...(params.sourcePath ? { authPath: params.sourcePath } : {}),
});
return [];
}
function findMatchingProfile(
@@ -402,56 +352,73 @@ export async function buildAuthItems(params: {
source: HermesSource;
targets: PlannedTargets;
}): Promise<MigrationItem[]> {
const items: MigrationItem[] = [];
if (await hasLegacyHermesAuthJson(params.source.authPath)) {
items.push(
createMigrationManualItem({
id: "manual:legacy-hermes-auth-json",
source: params.source.authPath ?? "auth.json",
message:
"Hermes auth.json contains legacy OAuth credentials. OpenClaw no longer imports those into live auth during Hermes migration.",
recommendation:
"Run openclaw models auth login --provider openai after migration, or run openclaw doctor --fix for existing OpenClaw legacy auth state.",
}),
);
}
const profiles = await readCodexAuthProfilesFromSource(params.source);
if (profiles.length === 0) {
return [];
return items;
}
const store = loadAuthProfileStoreWithoutExternalProfiles(params.targets.agentDir);
return profiles.map((profile) => {
const matchedProfileId = findMatchingProfile(store, profile.credential);
const profileId = matchedProfileId ?? profile.sourceProfileId;
const targetExists = Boolean(store.profiles[profileId]);
const skipped = !params.ctx.includeSecrets;
const configConflict = hasAuthProfileConfigConflict(
params.ctx.config,
oauthAuthProfileConfig(profileId, profile.credential),
Boolean(params.ctx.overwrite),
);
const conflict =
((targetExists && !matchedProfileId && !params.ctx.overwrite) || configConflict) && !skipped;
const itemId =
profiles.length === 1
? `auth:${OPENAI_PROVIDER_ID}`
: `auth:${OPENAI_PROVIDER_ID}:${profile.sourceProfileId}`;
return createMigrationItem({
id: itemId,
kind: "auth",
action: skipped ? "skip" : "create",
source: profile.candidate.sourcePath,
target: `${params.targets.agentDir}/auth-profiles.json#${profileId}`,
status: skipped ? "skipped" : conflict ? "conflict" : "planned",
sensitive: true,
reason: skipped
? HERMES_REASON_INCLUDE_SECRETS
: conflict
? HERMES_REASON_AUTH_PROFILE_EXISTS
: undefined,
message: skipped
? "OpenAI Codex OAuth credentials detected in Hermes."
: "Import Hermes OpenAI Codex OAuth credentials and configure OpenAI Codex models.",
details: {
provider: OPENAI_PROVIDER_ID,
profileId,
...(typeof profile.candidate.sourceCredentialIndex === "number"
? { sourceCredentialIndex: profile.candidate.sourceCredentialIndex }
: {}),
sourceCredentialFingerprint: sourceCredentialFingerprint(profile.candidate),
sourceProfileId: profile.sourceProfileId,
sourceKind: profile.candidate.sourceKind,
sourceLabel: profile.candidate.sourceLabel,
},
});
});
items.push(
...profiles.map((profile) => {
const matchedProfileId = findMatchingProfile(store, profile.credential);
const profileId = matchedProfileId ?? profile.sourceProfileId;
const targetExists = Boolean(store.profiles[profileId]);
const skipped = !params.ctx.includeSecrets;
const configConflict = hasAuthProfileConfigConflict(
params.ctx.config,
oauthAuthProfileConfig(profileId, profile.credential),
Boolean(params.ctx.overwrite),
);
const conflict =
((targetExists && !matchedProfileId && !params.ctx.overwrite) || configConflict) &&
!skipped;
const itemId =
profiles.length === 1
? `auth:${OPENAI_PROVIDER_ID}`
: `auth:${OPENAI_PROVIDER_ID}:${profile.sourceProfileId}`;
return createMigrationItem({
id: itemId,
kind: "auth",
action: skipped ? "skip" : "create",
source: profile.candidate.sourcePath,
target: `${params.targets.agentDir}/auth-profiles.json#${profileId}`,
status: skipped ? "skipped" : conflict ? "conflict" : "planned",
sensitive: true,
reason: skipped
? HERMES_REASON_INCLUDE_SECRETS
: conflict
? HERMES_REASON_AUTH_PROFILE_EXISTS
: undefined,
message: skipped
? "OpenAI OAuth credentials detected in OpenCode."
: "Import OpenAI OAuth credentials and configure OpenAI models.",
details: {
provider: OPENAI_PROVIDER_ID,
profileId,
...(typeof profile.candidate.sourceCredentialIndex === "number"
? { sourceCredentialIndex: profile.candidate.sourceCredentialIndex }
: {}),
sourceCredentialFingerprint: sourceCredentialFingerprint(profile.candidate),
sourceProfileId: profile.sourceProfileId,
sourceKind: profile.candidate.sourceKind,
sourceLabel: profile.candidate.sourceLabel,
},
});
}),
);
return items;
}
export async function applyAuthItem(
@@ -182,4 +182,69 @@ describe("Hermes migration file and skill items", () => {
await expectPathMissing(path.join(reportDir, "archive", "auth.json"));
await expectPathMissing(path.join(workspaceDir, "logs", "session.log"));
});
it("reports legacy Hermes auth.json OAuth state as manual reauth work", async () => {
const root = await makeTempRoot();
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const legacyOpenAIProvider = ["openai", "codex"].join("-");
await writeFile(
path.join(source, "auth.json"),
JSON.stringify({
providers: {
[legacyOpenAIProvider]: {
tokens: {
access_token: "old-access",
refresh_token: "old-refresh",
},
},
},
credential_pool: {
[legacyOpenAIProvider]: [
{
access_token: "pool-access",
refresh_token: "pool-refresh",
},
],
},
}),
);
const provider = buildHermesMigrationProvider();
const plan = await provider.plan(
makeContext({ source, stateDir, workspaceDir, includeSecrets: true }),
);
const manualAuth = itemById(plan.items, "manual:legacy-hermes-auth-json");
expect(manualAuth?.kind).toBe("manual");
expect(manualAuth?.status).toBe("skipped");
expect(manualAuth?.message).toContain("no longer imports");
expect(plan.items.some((item) => item.kind === "auth")).toBe(false);
expect(plan.warnings).toContain(
"Some Hermes settings require manual review before they can be activated safely.",
);
});
it("ignores empty Hermes auth.json credential containers", async () => {
const root = await makeTempRoot();
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
await writeFile(
path.join(source, "auth.json"),
JSON.stringify({
providers: {},
credential_pool: {},
tokens: { anthropic: { access: "other-access", refresh: "other-refresh" } },
}),
);
const provider = buildHermesMigrationProvider();
const plan = await provider.plan(
makeContext({ source, stateDir, workspaceDir, includeSecrets: true }),
);
expect(plan.items.find((item) => item.id === "manual:legacy-hermes-auth-json")).toBeUndefined();
});
});
@@ -125,17 +125,15 @@ describe("Hermes migration provider secret write failures", () => {
chatgpt_plan_type: "plus",
},
});
await writeFile(path.join(source, "auth.json"), "{}");
const opencodeAuthPath = path.join(root, ".local", "share", "opencode", "auth.json");
await writeFile(
path.join(source, "auth.json"),
opencodeAuthPath,
JSON.stringify({
providers: {
"openai-codex": {
last_refresh: new Date().toISOString(),
tokens: {
access_token: accessToken,
refresh_token: "refresh-fail-token",
},
},
openai: {
type: "oauth",
access: accessToken,
refresh: "refresh-fail-token",
},
}),
);
@@ -155,7 +153,7 @@ describe("Hermes migration provider secret write failures", () => {
id: "auth:openai",
kind: "auth",
action: "create",
source: path.join(source, "auth.json"),
source: opencodeAuthPath,
target: `${path.join(stateDir, "agents", "main", "agent")}/auth-profiles.json#openai:account-acct_fail`,
status: "error",
sensitive: true,
+16 -331
View File
@@ -354,96 +354,6 @@ describe("Hermes migration secret items", () => {
await expectMissingPath(path.join(agentDir, "auth-profiles.json"));
});
it("imports Hermes auth.json OpenAI Codex OAuth and configures models", async () => {
const root = await makeTempRoot();
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const reportDir = path.join(root, "report");
const agentDir = path.join(stateDir, "agents", "main", "agent");
const accessToken = fakeJwt({
exp: Math.floor(Date.now() / 1000) + 3600,
"https://api.openai.com/profile": { email: "codex@example.test" },
"https://api.openai.com/auth": {
chatgpt_account_id: "acct_test",
chatgpt_plan_type: "plus",
},
});
const config = {
agents: {
defaults: {
workspace: workspaceDir,
},
},
} as OpenClawConfig;
await writeFile(
path.join(source, "auth.json"),
JSON.stringify({
providers: {
"openai-codex": {
last_refresh: new Date().toISOString(),
tokens: {
access_token: accessToken,
refresh_token: "refresh-test-token",
},
},
},
}),
);
const provider = buildHermesMigrationProvider();
const ctx = makeContext({
source,
stateDir,
workspaceDir,
config,
includeSecrets: true,
reportDir,
runtime: makeConfigRuntime(config),
});
const plan = await provider.plan(ctx);
expect(plan.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "auth:openai",
kind: "auth",
status: "planned",
sensitive: true,
}),
]),
);
const result = await provider.apply(ctx, plan);
expect(result.summary.errors).toBe(0);
expect(result.summary.migrated).toBeGreaterThanOrEqual(1);
const authStore = JSON.parse(
await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf8"),
) as {
profiles?: Record<
string,
{ access?: string; provider?: string; refresh?: string; type?: string }
>;
};
const profile = authStore.profiles?.["openai:account-acct_test"];
expect(profile).toEqual(
expect.objectContaining({
type: "oauth",
provider: "openai",
access: accessToken,
refresh: "refresh-test-token",
}),
);
expect(config.auth?.profiles?.["openai:account-acct_test"]).toEqual(
expect.objectContaining({
provider: "openai",
mode: "oauth",
}),
);
expect(config.agents?.defaults?.models?.["openai/gpt-5.5"]).toEqual({});
});
it("imports supported Hermes provider env credentials including OpenCode and GitHub Copilot", async () => {
const root = await makeTempRoot();
const source = path.join(root, "hermes");
@@ -823,7 +733,7 @@ describe("Hermes migration secret items", () => {
}
});
it("imports OpenCode OpenAI OAuth credentials as OpenAI Codex auth", async () => {
it("imports OpenCode OpenAI OAuth credentials as OpenAI auth", async () => {
const root = await makeTempRoot();
const source = path.join(root, ".hermes");
const workspaceDir = path.join(root, "workspace");
@@ -837,7 +747,7 @@ describe("Hermes migration secret items", () => {
chatgpt_plan_type: "plus",
},
});
await writeFile(path.join(source, "config.yaml"), "model: openai/gpt-5.5\n");
await writeFile(path.join(source, "auth.json"), "{}");
await writeFile(
path.join(root, ".local", "share", "opencode", "auth.json"),
JSON.stringify({
@@ -908,87 +818,6 @@ describe("Hermes migration secret items", () => {
);
});
it("applies mixed Hermes and OpenCode OpenAI OAuth credentials with source-stable fingerprints", async () => {
const root = await makeTempRoot();
const source = path.join(root, ".hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const reportDir = path.join(root, "report");
const agentDir = path.join(stateDir, "agents", "main", "agent");
await writeFile(
path.join(source, "auth.json"),
JSON.stringify({
providers: {
"openai-codex": {
tokens: {
access_token: "opaque-hermes-access",
refresh_token: "opaque-hermes-refresh",
},
},
},
}),
);
await writeFile(
path.join(root, ".local", "share", "opencode", "auth.json"),
JSON.stringify({
openai: {
type: "oauth",
access: "opaque-opencode-access",
refresh: "opaque-opencode-refresh",
},
}),
);
const provider = buildHermesMigrationProvider();
const ctx = makeContext({
source,
stateDir,
workspaceDir,
includeSecrets: true,
reportDir,
});
const plan = await provider.plan(ctx);
const authItems = plan.items.filter((item) => item.kind === "auth");
expect(authItems).toHaveLength(2);
expect(authItems.map((item) => item.status)).toEqual(["planned", "planned"]);
expect(authItems.map((item) => item.details?.sourceCredentialIndex)).toEqual([0, 0]);
expect(authItems.map((item) => item.details?.sourceCredentialFingerprint)).toEqual([
expect.any(String),
expect.any(String),
]);
const result = await provider.apply(ctx, plan);
expect(
result.items
.filter((item) => item.kind === "auth")
.map((item) => ({ id: item.id, status: item.status })),
).toEqual([
{ 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<string, { access?: string; provider?: string; refresh?: string }>;
};
expect(authStore.profiles?.["openai:hermes-import-1"]).toEqual(
expect.objectContaining({
provider: "openai",
access: "opaque-hermes-access",
refresh: "opaque-hermes-refresh",
}),
);
expect(authStore.profiles?.["openai:hermes-import-2"]).toEqual(
expect.objectContaining({
provider: "openai",
access: "opaque-opencode-access",
refresh: "opaque-opencode-refresh",
}),
);
});
it("does not apply a planned OpenCode OpenAI OAuth credential after the source token changes", async () => {
const root = await makeTempRoot();
const source = path.join(root, ".hermes");
@@ -1054,7 +883,7 @@ describe("Hermes migration secret items", () => {
await expectMissingPath(path.join(agentDir, "auth-profiles.json"));
});
it("reports Hermes OAuth config auth profile conflicts during planning", async () => {
it("reports OpenCode OpenAI OAuth config auth profile conflicts during planning", async () => {
const root = await makeTempRoot();
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
@@ -1082,17 +911,14 @@ describe("Hermes migration secret items", () => {
},
},
} as OpenClawConfig;
await writeFile(path.join(source, "auth.json"), "{}");
await writeFile(
path.join(source, "auth.json"),
path.join(root, ".local", "share", "opencode", "auth.json"),
JSON.stringify({
providers: {
"openai-codex": {
last_refresh: new Date().toISOString(),
tokens: {
access_token: accessToken,
refresh_token: "refresh-test-token",
},
},
openai: {
type: "oauth",
access: accessToken,
refresh: "refresh-test-token",
},
}),
);
@@ -1120,145 +946,7 @@ describe("Hermes migration secret items", () => {
);
});
it("imports every distinct Hermes auth.json OpenAI Codex OAuth credential", async () => {
const root = await makeTempRoot();
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
const stateDir = path.join(root, "state");
const reportDir = path.join(root, "report");
const agentDir = path.join(stateDir, "agents", "main", "agent");
const activeAccessToken = fakeJwt({
exp: Math.floor(Date.now() / 1000) + 3600,
"https://api.openai.com/profile": { email: "active@example.test" },
"https://api.openai.com/auth": {
chatgpt_account_id: "acct_active",
chatgpt_plan_type: "plus",
},
});
const poolAccessToken = fakeJwt({
exp: Math.floor(Date.now() / 1000) + 3600,
"https://api.openai.com/profile": { email: "pool@example.test" },
"https://api.openai.com/auth": {
chatgpt_account_id: "acct_pool",
chatgpt_plan_type: "team",
},
});
const secondPoolAccessToken = fakeJwt({
exp: Math.floor(Date.now() / 1000) + 3600,
"https://api.openai.com/profile": { email: "second-pool@example.test" },
"https://api.openai.com/auth": {
chatgpt_account_id: "acct_second_pool",
chatgpt_plan_type: "pro",
},
});
const config = {
agents: {
defaults: {
workspace: workspaceDir,
},
},
} as OpenClawConfig;
await writeFile(
path.join(source, "auth.json"),
JSON.stringify({
providers: {
"openai-codex": {
last_refresh: "2026-01-03T00:00:00.000Z",
tokens: {
access_token: activeAccessToken,
refresh_token: "refresh-active-token",
},
},
},
credential_pool: {
"openai-codex": [
{
label: "Pool account",
last_refresh: "2026-01-02T00:00:00.000Z",
access_token: poolAccessToken,
refresh_token: "refresh-pool-token",
},
{
label: "Second pool account",
last_refresh: "2026-01-01T00:00:00.000Z",
access_token: secondPoolAccessToken,
refresh_token: "refresh-second-pool-token",
},
],
},
}),
);
const provider = buildHermesMigrationProvider();
const ctx = makeContext({
source,
stateDir,
workspaceDir,
config,
includeSecrets: true,
reportDir,
runtime: makeConfigRuntime(config),
});
const plan = await provider.plan(ctx);
const authItems = plan.items.filter((item) => item.kind === "auth");
expect(authItems).toHaveLength(3);
expect(
authItems
.map((item) => item.details?.profileId)
.toSorted((left, right) => String(left).localeCompare(String(right))),
).toEqual([
"openai:account-acct_active",
"openai:account-acct_pool",
"openai:account-acct_second_pool",
]);
const result = await provider.apply(ctx, plan);
expect(result.summary.errors).toBe(0);
const authStore = JSON.parse(
await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf8"),
) as {
profiles?: Record<
string,
{ access?: string; provider?: string; refresh?: string; type?: string }
>;
};
expect(authStore.profiles?.["openai:account-acct_active"]).toEqual(
expect.objectContaining({
type: "oauth",
provider: "openai",
access: activeAccessToken,
refresh: "refresh-active-token",
}),
);
expect(authStore.profiles?.["openai:account-acct_pool"]).toEqual(
expect.objectContaining({
type: "oauth",
provider: "openai",
access: poolAccessToken,
refresh: "refresh-pool-token",
}),
);
expect(authStore.profiles?.["openai:account-acct_second_pool"]).toEqual(
expect.objectContaining({
type: "oauth",
provider: "openai",
access: secondPoolAccessToken,
refresh: "refresh-second-pool-token",
}),
);
expect(
Object.keys(config.auth?.profiles ?? {}).toSorted((left, right) => left.localeCompare(right)),
).toEqual([
"openai:account-acct_active",
"openai:account-acct_pool",
"openai:account-acct_second_pool",
]);
expect(config.agents?.defaults?.models?.["openai/gpt-5.5"]).toEqual({});
});
it("does not collapse Hermes OAuth accounts that share an email", async () => {
it("does not collapse OpenCode OpenAI OAuth accounts that share an email", async () => {
const root = await makeTempRoot();
const source = path.join(root, "hermes");
const workspaceDir = path.join(root, "workspace");
@@ -1281,17 +969,14 @@ describe("Hermes migration secret items", () => {
},
},
} as OpenClawConfig;
await writeFile(path.join(source, "config.yaml"), "model: openai/gpt-5.5\n");
await writeFile(
path.join(source, "auth.json"),
path.join(root, ".local", "share", "opencode", "auth.json"),
JSON.stringify({
providers: {
"openai-codex": {
last_refresh: new Date().toISOString(),
tokens: {
access_token: accessToken,
refresh_token: "refresh-new-token",
},
},
openai: {
type: "oauth",
access: accessToken,
refresh: "refresh-new-token",
},
}),
);
+3 -3
View File
@@ -9,9 +9,9 @@ export {
OPENAI_DEFAULT_TTS_MODEL,
OPENAI_DEFAULT_TTS_VOICE,
} from "./default-models.js";
export { buildOpenAICodexProvider } from "./openai-codex-catalog.js";
export { loginOpenAICodexOAuth } from "./openai-codex-oauth.runtime.js";
export { refreshOpenAICodexToken } from "./openai-codex-provider.runtime.js";
export { buildOpenAICodexProvider } from "./openai-chatgpt-catalog.js";
export { loginOpenAICodexOAuth } from "./openai-chatgpt-oauth.runtime.js";
export { refreshOpenAICodexToken } from "./openai-chatgpt-provider.runtime.js";
export { buildOpenAICodexProviderPlugin, buildOpenAIProvider } from "./openai-provider.js";
export { buildOpenAIRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
export { buildOpenAIRealtimeVoiceProvider } from "./realtime-voice-provider.js";
@@ -18,10 +18,12 @@ const {
(params: { provider: string; agentDir?: string }) => boolean
>(() => false),
listProfilesForProviderMock: vi.fn(
(store: { profiles?: Record<string, { provider?: string }> }, provider: string) =>
Object.entries(store.profiles ?? {})
.filter(([, profile]) => profile.provider === provider)
.map(([profileId]) => profileId),
(store: { profiles?: Record<string, { provider?: string }> }, provider: string) => {
const normalize = (raw?: string) => (raw === ["openai", "codex"].join("-") ? "openai" : raw);
return Object.entries(store.profiles ?? {})
.filter(([, profile]) => normalize(profile.provider) === provider)
.map(([profileId]) => profileId);
},
),
resolveApiKeyForProviderMock: vi.fn(
async (_params?: {
@@ -156,8 +158,8 @@ function mockCodexRawStream(body: string) {
function mockCodexAuthOnly() {
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai-codex") {
return { apiKey: "codex-key", source: "profile:openai-codex:default", mode: "oauth" };
if (params?.provider === "openai") {
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
@@ -167,9 +169,9 @@ function createCodexOAuthAuthStore() {
return {
version: 1 as const,
profiles: {
"openai-codex:default": {
"openai:default": {
type: "oauth" as const,
provider: "openai-codex",
provider: "openai",
access: "codex-access",
refresh: "codex-refresh",
expires: Date.now() + 60_000,
@@ -182,9 +184,9 @@ function createCodexApiKeyAuthStore() {
return {
version: 1 as const,
profiles: {
"openai-codex:manual": {
"openai:manual": {
type: "api_key" as const,
provider: "openai-codex",
provider: "openai",
key: "codex-api-key",
},
},
@@ -195,9 +197,9 @@ function createCodexTokenAuthStore() {
return {
version: 1 as const,
profiles: {
"openai-codex:token": {
"openai:token": {
type: "token" as const,
provider: "openai-codex",
provider: "openai",
token: "codex-token",
},
},
@@ -359,9 +361,7 @@ describe("openai image generation provider", () => {
it("reports configured when either OpenAI API key auth or Codex OAuth auth is available", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai";
});
isProviderApiKeyConfiguredMock.mockReturnValue(true);
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai",
@@ -369,29 +369,46 @@ describe("openai image generation provider", () => {
});
isProviderApiKeyConfiguredMock.mockClear();
isProviderApiKeyConfiguredMock.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai-codex";
});
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue(createCodexOAuthAuthStore());
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai",
agentDir: "/tmp/agent",
});
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai-codex",
provider: "openai",
agentDir: "/tmp/agent",
});
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue({ version: 1, profiles: {} });
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(false);
});
it("reports configured before doctor rewrites retired OpenAI auth profiles", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue({
version: 1,
profiles: {
"openai:default": {
type: "oauth",
provider: ["openai", "codex"].join("-"),
access: "legacy-chatgpt-access",
refresh: "legacy-chatgpt-refresh",
expires: Date.now() + 60_000,
},
},
});
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true);
});
it("does not report Codex OAuth image auth as configured for custom OpenAI endpoints", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai-codex";
});
isProviderApiKeyConfiguredMock.mockReturnValue(false);
expect(
provider.isConfigured?.({
@@ -410,12 +427,50 @@ describe("openai image generation provider", () => {
).toBe(false);
});
it("reports ChatGPT OAuth image auth as configured for ChatGPT routes", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue(createCodexOAuthAuthStore());
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://chatgpt.com/backend-api/codex",
models: [],
},
},
},
},
}),
).toBe(true);
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
api: "openai-chatgpt-responses",
baseUrl: "https://openai-compatible.example.test/v1",
models: [],
},
},
},
},
}),
).toBe(true);
});
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";
});
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue({
version: 1,
profiles: {
@@ -449,9 +504,7 @@ describe("openai image generation provider", () => {
it("does not report Codex OAuth image auth as configured for non-exact public OpenAI URLs", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai-codex";
});
isProviderApiKeyConfiguredMock.mockReturnValue(false);
expect(
provider.isConfigured?.({
@@ -917,8 +970,6 @@ describe("openai image generation provider", () => {
expect(authResolutionCall(0).provider).toBe("openai");
expect(authResolutionCall(0).store).toBe(authStore);
expect(authResolutionCall(1).provider).toBe("openai-codex");
expect(authResolutionCall(1).store).toBe(authStore);
const configCall = httpConfigCall();
expect(configCall.defaultBaseUrl).toBe("https://chatgpt.com/backend-api/codex");
expect(configCall.defaultHeaders).toEqual({
@@ -926,7 +977,7 @@ describe("openai image generation provider", () => {
Accept: "text/event-stream",
});
expect(configCall.provider).toBe("openai");
expect(configCall.api).toBe("openai-codex-responses");
expect(configCall.api).toBe("openai-chatgpt-responses");
expect(configCall.capability).toBe("image");
const request = jsonRequestCall();
const body = request.body as Record<string, unknown>;
@@ -1001,12 +1052,9 @@ describe("openai image generation provider", () => {
mockGeneratedPngResponse();
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return {};
}
if (params?.provider === "openai-codex") {
return {
apiKey: "codex-api-key",
source: "profile:openai-codex:manual",
source: "profile:openai:manual",
mode: "api-key",
};
}
@@ -1022,9 +1070,8 @@ describe("openai image generation provider", () => {
authStore: createCodexApiKeyAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(2);
expect(authResolutionCall(0).provider).toBe("openai");
expect(authResolutionCall(1).provider).toBe("openai-codex");
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
const configCall = httpConfigCall();
expect(configCall.defaultBaseUrl).toBe("https://api.openai.com/v1");
expect(configCall.defaultHeaders).toEqual({
@@ -1042,10 +1089,10 @@ describe("openai image generation provider", () => {
it("uses native OpenAI image requests when mixed Codex profiles resolve to an API key", async () => {
mockGeneratedPngResponse();
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai-codex") {
if (params?.provider === "openai") {
return {
apiKey: "codex-api-key",
source: "profile:openai-codex:manual",
source: "profile:openai:manual",
mode: "api-key",
};
}
@@ -1062,7 +1109,7 @@ describe("openai image generation provider", () => {
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai-codex");
expect(authResolutionCall().provider).toBe("openai");
expect(httpConfigCall().defaultBaseUrl).toBe("https://api.openai.com/v1");
expect(httpConfigCall().defaultHeaders).toEqual({
Authorization: "Bearer codex-api-key",
@@ -1078,12 +1125,9 @@ describe("openai image generation provider", () => {
mockCodexImageStream({ imageData: "codex-token-image" });
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return {};
}
if (params?.provider === "openai-codex") {
return {
apiKey: "codex-token",
source: "profile:openai-codex:token",
source: "profile:openai:token",
mode: "token",
};
}
@@ -1100,10 +1144,10 @@ describe("openai image generation provider", () => {
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai-codex");
expect(authResolutionCall().provider).toBe("openai");
expect(httpConfigCall().defaultBaseUrl).toBe("https://chatgpt.com/backend-api/codex");
expect(httpConfigCall().provider).toBe("openai");
expect(httpConfigCall().api).toBe("openai-codex-responses");
expect(httpConfigCall().api).toBe("openai-chatgpt-responses");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
expect(postMultipartRequestMock).not.toHaveBeenCalled();
expect(result.images[0]?.buffer).toEqual(Buffer.from("codex-token-image"));
@@ -1113,12 +1157,9 @@ describe("openai image generation provider", () => {
mockCodexImageStream({ imageData: "codex-token-image" });
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return { apiKey: "openai-key", source: "OPENAI_API_KEY", mode: "api-key" };
}
if (params?.provider === "openai-codex") {
return {
apiKey: "codex-token",
source: "profile:openai-codex:token",
source: "profile:openai:token",
mode: "token",
};
}
@@ -1135,12 +1176,7 @@ describe("openai image generation provider", () => {
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai-codex");
expect(
resolveApiKeyForProviderMock.mock.calls.some(
([call]) => (call as AuthResolutionCall).provider === "openai",
),
).toBe(false);
expect(authResolutionCall().provider).toBe("openai");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
});
@@ -1200,10 +1236,7 @@ describe("openai image generation provider", () => {
it("uses configured Codex OAuth directly instead of probing an available OpenAI API key", async () => {
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return { apiKey: "openai-key", source: "OPENAI_API_KEY", mode: "api-key" };
}
if (params?.provider === "openai-codex") {
return { apiKey: "codex-key", source: "profile:openai-codex:default", mode: "oauth" };
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
@@ -1220,13 +1253,8 @@ describe("openai image generation provider", () => {
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai-codex");
expect(authResolutionCall().provider).toBe("openai");
expect(authResolutionCall().store).toBe(authStore);
expect(
resolveApiKeyForProviderMock.mock.calls.some(
([call]) => (call as AuthResolutionCall).provider === "openai",
),
).toBe(false);
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
expect(logInfoMock).toHaveBeenCalledWith(
"image auth selected: provider=openai mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.5 timeoutMs=180000",
@@ -1337,9 +1365,6 @@ describe("openai image generation provider", () => {
if (params?.provider === "openai") {
throw new Error("Keychain unavailable");
}
if (params?.provider === "openai-codex") {
return { apiKey: "codex-key", source: "profile:openai-codex:default", mode: "oauth" };
}
return {};
});
mockCodexImageStream({ imageData: "codex-image" });
@@ -1361,10 +1386,10 @@ describe("openai image generation provider", () => {
it("sanitizes Codex OAuth image auth log values", async () => {
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai-codex") {
if (params?.provider === "openai") {
return {
apiKey: "codex-key",
source: "profile:openai-codex:default",
source: "profile:openai:default",
mode: "oauth\nfake\u202eignored",
};
}
@@ -1432,9 +1457,9 @@ describe("openai image generation provider", () => {
cfg: {
models: {
providers: {
"openai-codex": {
openai: {
baseUrl: "http://127.0.0.1:44220/backend-api/codex",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
request: { allowPrivateNetwork: true },
models: [],
},
@@ -1473,9 +1498,9 @@ describe("openai image generation provider", () => {
cfg: {
models: {
providers: {
"openai-codex": {
openai: {
baseUrl: configuredBaseUrl,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
models: [],
},
},
@@ -1486,7 +1511,7 @@ describe("openai image generation provider", () => {
expect(httpConfigCall().baseUrl).toBe("https://chatgpt.com/backend-api/codex");
expect(httpConfigCall().provider).toBe("openai");
expect(httpConfigCall().api).toBe("openai-codex-responses");
expect(httpConfigCall().api).toBe("openai-chatgpt-responses");
expect(httpConfigCall().capability).toBe("image");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
});
@@ -1497,8 +1522,8 @@ describe("openai image generation provider", () => {
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" };
if (params?.provider === "openai") {
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
@@ -1526,11 +1551,6 @@ describe("openai image generation provider", () => {
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(authResolutionCall().store).toBe(authStore);
expect(
resolveApiKeyForProviderMock.mock.calls.some(
([call]) => (call as AuthResolutionCall).provider === "openai-codex",
),
).toBe(false);
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
});
@@ -1540,8 +1560,8 @@ describe("openai image generation provider", () => {
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" };
if (params?.provider === "openai") {
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
@@ -1612,7 +1632,7 @@ describe("openai image generation provider", () => {
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(httpConfigCall().provider).toBe("openai");
expect(httpConfigCall().api).toBe("openai-codex-responses");
expect(httpConfigCall().api).toBe("openai-chatgpt-responses");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
});
+36 -66
View File
@@ -27,7 +27,11 @@ import {
sanitizeConfiguredModelProviderRequest,
} from "openclaw/plugin-sdk/provider-http";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import { canonicalizeCodexResponsesBaseUrl, OPENAI_CODEX_RESPONSES_BASE_URL } from "./base-url.js";
import {
canonicalizeCodexResponsesBaseUrl,
isOpenAICodexBaseUrl,
OPENAI_CODEX_RESPONSES_BASE_URL,
} from "./base-url.js";
import { OPENAI_DEFAULT_IMAGE_MODEL as DEFAULT_OPENAI_IMAGE_MODEL } from "./default-models.js";
import { resolveConfiguredOpenAIBaseUrl } from "./shared.js";
@@ -293,10 +297,6 @@ function shouldAllowPrivateImageEndpoint(req: {
return process.env.OPENCLAW_QA_ALLOW_LOCAL_IMAGE_PROVIDER === "1";
}
function normalizeProviderId(value: string | undefined): string {
return value?.trim().toLowerCase() ?? "";
}
function resolveRequestAuthStore(req: {
authStore?: AuthProfileStore;
agentDir?: string;
@@ -333,7 +333,7 @@ function hasDirectOpenAIImageApiKeyAuth(params: {
}
const profileIds = listProfilesForProvider(store, "openai");
if (profileIds.length === 0) {
return true;
return false;
}
return profileIds.some((profileId) => store.profiles[profileId]?.type === "api_key");
}
@@ -346,11 +346,9 @@ function hasCodexResponseTransportProfileConfigured(req: {
if (!store) {
return false;
}
return ["openai", "openai-codex"].some((provider) =>
listProfilesForProvider(store, provider).some(
(profileId) =>
store.profiles[profileId]?.type === "oauth" || store.profiles[profileId]?.type === "token",
),
return listProfilesForProvider(store, "openai").some(
(profileId) =>
store.profiles[profileId]?.type === "oauth" || store.profiles[profileId]?.type === "token",
);
}
@@ -367,16 +365,7 @@ function resolveOpenAIImageAuthProvider(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";
return "openai";
}
function hasExplicitOpenAIImageApiKeyConfig(cfg: OpenClawConfig | undefined): boolean {
@@ -394,7 +383,15 @@ function hasExplicitDirectOpenAIImageConfig(cfg: OpenClawConfig | undefined): bo
providerConfig.headers !== undefined ||
providerConfig.authHeader !== undefined ||
providerConfig.request !== undefined ||
(providerConfig.api !== undefined && providerConfig.api !== "openai-codex-responses")
(providerConfig.api !== undefined && providerConfig.api !== "openai-chatgpt-responses")
);
}
function hasChatGPTImageRouteConfig(cfg: OpenClawConfig | undefined): boolean {
const providerConfig = cfg?.models?.providers?.openai;
return (
isOpenAICodexBaseUrl(resolveConfiguredOpenAIBaseUrl(cfg)) ||
providerConfig?.api === "openai-chatgpt-responses"
);
}
@@ -441,18 +438,8 @@ async function resolveOpenAIImageAuth(req: {
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",
provider: resolveOpenAIImageAuthProvider(req),
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore,
@@ -717,11 +704,8 @@ async function generateOpenAICodexImage(params: {
const { req, apiKey } = params;
const inputImages = req.inputImages ?? [];
const openAIProviderConfig = req.cfg?.models?.providers?.openai;
const legacyCodexProviderConfig = req.cfg?.models?.providers?.["openai-codex"];
const codexProviderConfig =
openAIProviderConfig?.api === "openai-codex-responses"
? openAIProviderConfig
: legacyCodexProviderConfig;
openAIProviderConfig?.api === "openai-chatgpt-responses" ? openAIProviderConfig : undefined;
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: canonicalizeCodexResponsesBaseUrl(codexProviderConfig?.baseUrl),
@@ -732,7 +716,7 @@ async function generateOpenAICodexImage(params: {
},
request: sanitizeConfiguredModelProviderRequest(codexProviderConfig?.request),
provider: "openai",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
capability: "image",
transport: "http",
});
@@ -830,6 +814,9 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider {
id: "openai",
label: "OpenAI",
isConfigured: ({ cfg, agentDir }) => {
const configuredBaseUrl = resolveConfiguredOpenAIBaseUrl(cfg);
const hasPublicOpenAIBaseUrl = isPublicOpenAIImageBaseUrl(configuredBaseUrl);
const hasChatGPTRouteConfig = hasChatGPTImageRouteConfig(cfg);
if (
isProviderApiKeyConfigured({
provider: "openai",
@@ -837,32 +824,29 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider {
})
) {
return (
isPublicOpenAIImageBaseUrl(resolveConfiguredOpenAIBaseUrl(cfg)) ||
hasDirectOpenAIImageApiKeyAuth({ cfg, agentDir })
hasPublicOpenAIBaseUrl ||
hasDirectOpenAIImageApiKeyAuth({ cfg, agentDir }) ||
(hasChatGPTRouteConfig && hasCodexResponseTransportProfileConfigured({ agentDir }))
);
}
if (
isProviderApiKeyConfigured({
provider: "openai-codex",
agentDir,
})
) {
return isPublicOpenAIImageBaseUrl(resolveConfiguredOpenAIBaseUrl(cfg));
}
if (!isPublicOpenAIImageBaseUrl(resolveConfiguredOpenAIBaseUrl(cfg))) {
if (!hasPublicOpenAIBaseUrl && !hasChatGPTRouteConfig) {
return false;
}
return false;
return hasCodexResponseTransportProfileConfigured({ agentDir });
},
async generateImage(req) {
const inputImages = req.inputImages ?? [];
const isEdit = inputImages.length > 0;
const rawBaseUrl = resolveConfiguredOpenAIBaseUrl(req.cfg);
const publicOpenAIBaseUrl = isPublicOpenAIImageBaseUrl(rawBaseUrl);
const chatGPTBaseUrl = isOpenAICodexBaseUrl(rawBaseUrl);
const codexResponsesConfigured =
req.cfg?.models?.providers?.openai?.api === "openai-chatgpt-responses";
const explicitOpenAIApiKeyConfig = hasExplicitOpenAIImageApiKeyConfig(req.cfg);
const explicitDirectOpenAIConfig = hasExplicitDirectOpenAIImageConfig(req.cfg);
const explicitDirectOpenAIConfig =
!chatGPTBaseUrl && !codexResponsesConfigured && hasExplicitDirectOpenAIImageConfig(req.cfg);
const useCodexResponseTransportRoute =
publicOpenAIBaseUrl &&
(publicOpenAIBaseUrl || chatGPTBaseUrl || codexResponsesConfigured) &&
!explicitDirectOpenAIConfig &&
hasCodexResponseTransportProfileConfigured(req);
let preResolvedImageAuth:
@@ -920,20 +904,6 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider {
}
imageAuth = undefined;
}
if (!imageAuth?.apiKey && publicOpenAIBaseUrl && !explicitDirectOpenAIConfig) {
const legacyCodexAuth = await resolveOptionalApiKeyForProvider({
provider: "openai-codex",
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore,
});
if (legacyCodexAuth?.apiKey && isCodexSubscriptionAuthMode(legacyCodexAuth.mode)) {
const timeoutMs = resolveOpenAIImageTimeoutMs(req.timeoutMs);
logCodexImageAuthSelected({ req, authMode: legacyCodexAuth.mode, timeoutMs });
return generateOpenAICodexImage({ req, apiKey: legacyCodexAuth.apiKey });
}
imageAuth = legacyCodexAuth;
}
if (!imageAuth?.apiKey) {
if (!publicOpenAIBaseUrl) {
throw new Error("OpenAI API key missing");
+6 -6
View File
@@ -32,11 +32,11 @@ vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
};
});
vi.mock("./openai-codex-oauth-flow.runtime.js", () => ({
vi.mock("./openai-chatgpt-oauth-flow.runtime.js", () => ({
refreshOpenAICodexToken: runtimeMocks.refreshOpenAICodexToken,
}));
import { createOpenAICodexProviderRuntime } from "./openai-codex-provider.runtime.js";
import { createOpenAICodexProviderRuntime } from "./openai-chatgpt-provider.runtime.js";
const registerOpenAIPluginForTest = async () =>
registerProviderPlugin({
@@ -354,10 +354,10 @@ describe("openai plugin", () => {
const normalizedCodex = openaiProvider.normalizeToolSchemas?.({
provider: "openai",
modelId: "gpt-5.4",
modelApi: "openai-codex-responses",
modelApi: "openai-chatgpt-responses",
model: {
provider: "openai",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api",
id: "gpt-5.4",
} as never,
@@ -394,10 +394,10 @@ describe("openai plugin", () => {
openaiProvider.inspectToolSchemas?.({
provider: "openai",
modelId: "gpt-5.4",
modelApi: "openai-codex-responses",
modelApi: "openai-chatgpt-responses",
model: {
provider: "openai",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api",
id: "gpt-5.4",
} as never,
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js";
import { resolveCodexAuthIdentity } from "./openai-chatgpt-auth-identity.js";
function createJwt(payload: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
@@ -1,5 +1,5 @@
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import { trimNonEmptyString } from "./openai-codex-shared.js";
import { trimNonEmptyString } from "./openai-chatgpt-shared.js";
type CodexJwtPayload = {
exp?: unknown;
@@ -6,7 +6,7 @@ const OPENAI_CODEX_BASE_URL = OPENAI_CODEX_RESPONSES_BASE_URL;
export function buildOpenAICodexProvider(): ModelProviderConfig {
return {
baseUrl: OPENAI_CODEX_BASE_URL,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
models: [],
};
}
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { resolveCodexAccessTokenExpiry } from "./openai-codex-auth-identity.js";
import { loginOpenAICodexDeviceCode } from "./openai-codex-device-code.js";
import { resolveCodexAccessTokenExpiry } from "./openai-chatgpt-auth-identity.js";
import { loginOpenAICodexDeviceCode } from "./openai-chatgpt-device-code.js";
function createJwt(payload: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
@@ -2,8 +2,8 @@ import {
positiveSecondsToSafeMilliseconds,
resolveExpiresAtMsFromDurationSeconds,
} from "openclaw/plugin-sdk/number-runtime";
import { resolveCodexAccessTokenExpiry } from "./openai-codex-auth-identity.js";
import { trimNonEmptyString } from "./openai-codex-shared.js";
import { resolveCodexAccessTokenExpiry } from "./openai-chatgpt-auth-identity.js";
import { trimNonEmptyString } from "./openai-chatgpt-shared.js";
const OPENAI_AUTH_BASE_URL = "https://auth.openai.com";
const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
@@ -8,7 +8,7 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: ssrfMocks.fetchWithSsrFGuard,
}));
import { openaiCodexOAuthProvider, testing } from "./openai-codex-oauth-flow.runtime.js";
import { openaiCodexOAuthProvider, testing } from "./openai-chatgpt-oauth-flow.runtime.js";
function timeoutError(): Error {
return new DOMException("timed out", "TimeoutError");
@@ -96,7 +96,7 @@ describe("OpenAI Codex OAuth flow", () => {
expect(ssrfMocks.fetchWithSsrFGuard).toHaveBeenCalledWith(
expect.objectContaining({
auditContext: "openai-codex-oauth-token",
auditContext: "openai-chatgpt-oauth-token",
timeoutMs: 5,
}),
);
@@ -149,7 +149,7 @@ describe("OpenAI Codex OAuth flow", () => {
expect(ssrfMocks.fetchWithSsrFGuard).toHaveBeenCalledWith(
expect.objectContaining({
auditContext: "openai-codex-oauth-token",
auditContext: "openai-chatgpt-oauth-token",
timeoutMs: 5,
}),
);
@@ -11,20 +11,20 @@ import {
resolveOAuthTokenLifetimeMs,
} from "openclaw/plugin-sdk/provider-oauth-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js";
import { resolveCodexAuthIdentity } from "./openai-chatgpt-auth-identity.js";
import {
createOAuthLoginCancelledError,
throwIfOAuthLoginAborted,
withOAuthLoginAbort,
} from "./openai-codex-oauth-abort.runtime.js";
import { oauthErrorHtml, oauthSuccessHtml } from "./openai-codex-oauth-page.runtime.js";
} from "./openai-chatgpt-oauth-abort.runtime.js";
import { oauthErrorHtml, oauthSuccessHtml } from "./openai-chatgpt-oauth-page.runtime.js";
import type {
OAuthCredentials,
OAuthLoginCallbacks,
OAuthPrompt,
OAuthProviderInterface,
} from "./openai-codex-oauth-types.runtime.js";
import { generatePKCE } from "./openai-codex-pkce.runtime.js";
} from "./openai-chatgpt-oauth-types.runtime.js";
import { generatePKCE } from "./openai-chatgpt-pkce.runtime.js";
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
@@ -175,7 +175,7 @@ async function postTokenForm(
},
timeoutMs,
signal: options.signal,
auditContext: "openai-codex-oauth-token",
auditContext: "openai-chatgpt-oauth-token",
});
try {
const responseBody = await response.arrayBuffer();
@@ -573,7 +573,7 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAu
}
export const openaiCodexOAuthProvider: OAuthProviderInterface = {
id: "openai-codex",
id: "openai",
name: "ChatGPT Plus/Pro (Codex Subscription)",
usesCallbackServer: true,
@@ -1,6 +1,6 @@
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { testing } from "./openai-codex-oauth.runtime.js";
import { testing } from "./openai-chatgpt-oauth.runtime.js";
describe("OpenAI Codex OAuth runtime", () => {
afterEach(() => {
@@ -4,8 +4,8 @@ import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import type { ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry";
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
import { formatCliCommand } from "openclaw/plugin-sdk/setup-tools";
import { loginOpenAICodex } from "./openai-codex-oauth-flow.runtime.js";
import type { OAuthCredentials } from "./openai-codex-oauth-types.runtime.js";
import { loginOpenAICodex } from "./openai-chatgpt-oauth-flow.runtime.js";
import type { OAuthCredentials } from "./openai-chatgpt-oauth-types.runtime.js";
const manualInputPromptMessage = "Paste the authorization code (or full redirect URL):";
const openAICodexOAuthOriginator = "openclaw";
@@ -1,6 +1,6 @@
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
import { refreshOpenAICodexToken as refreshOpenAICodexTokenFromFlow } from "./openai-codex-oauth-flow.runtime.js";
import type { OAuthCredentials } from "./openai-codex-oauth-types.runtime.js";
import { refreshOpenAICodexToken as refreshOpenAICodexTokenFromFlow } from "./openai-chatgpt-oauth-flow.runtime.js";
import type { OAuthCredentials } from "./openai-chatgpt-oauth-types.runtime.js";
type OpenAICodexProviderRuntimeDeps = {
ensureGlobalUndiciEnvProxyDispatcher: typeof ensureGlobalUndiciEnvProxyDispatcher;
@@ -46,10 +46,10 @@ async function getOpenAICodexOAuthApiKey(
providerId: string,
credentials: Record<string, OAuthCredentials>,
): Promise<{ newCredentials: OAuthCredentials; apiKey: string } | null> {
if (providerId !== "openai" && providerId !== "openai-codex") {
if (providerId !== "openai") {
throw new Error(`Unknown OAuth provider: ${providerId}`);
}
let creds = credentials[providerId] ?? credentials["openai-codex"];
let creds = credentials[providerId];
if (!creds) {
return null;
}
@@ -3,11 +3,11 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn());
const loginOpenAICodexDeviceCodeMock = vi.hoisted(() => vi.fn());
vi.mock("./openai-codex-provider.runtime.js", () => ({
vi.mock("./openai-chatgpt-provider.runtime.js", () => ({
refreshOpenAICodexToken: refreshOpenAICodexTokenMock,
}));
vi.mock("./openai-codex-device-code.js", () => ({
vi.mock("./openai-chatgpt-device-code.js", () => ({
loginOpenAICodexDeviceCode: loginOpenAICodexDeviceCodeMock,
}));
@@ -28,23 +28,14 @@ describe("OpenAI provider Codex transport hooks", () => {
expect(provider.id).toBe("openai");
expect(provider.aliases).toBeUndefined();
expect(provider.hookAliases).toEqual([
"openai-codex",
"azure-openai",
"azure-openai-responses",
]);
expect(provider.hookAliases).toEqual(["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",
},
]);
expect(provider.oauthProfileIdRepairs).toBeUndefined();
});
it("stores device-code logins as OpenAI OAuth profiles", async () => {
@@ -85,14 +76,14 @@ describe("OpenAI provider Codex transport hooks", () => {
const model = provider.resolveDynamicModel?.({
provider: "openai",
modelId: "gpt-5.4",
providerConfig: { api: "openai-codex-responses" },
providerConfig: { api: "openai-chatgpt-responses" },
modelRegistry: { find: () => null },
} as never);
expect(model).toMatchObject({
provider: "openai",
id: "gpt-5.4",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
});
});
@@ -103,7 +94,7 @@ describe("OpenAI provider Codex transport hooks", () => {
const model = provider.resolveDynamicModel?.({
provider: "openai",
modelId: "gpt-5.5",
providerConfig: { api: "openai-codex-responses" },
providerConfig: { api: "openai-chatgpt-responses" },
modelRegistry: {
find: () => ({
provider: "openai",
@@ -123,7 +114,7 @@ describe("OpenAI provider Codex transport hooks", () => {
expect(model).toMatchObject({
provider: "openai",
id: "gpt-5.5",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
});
});
@@ -134,7 +125,7 @@ describe("OpenAI provider Codex transport hooks", () => {
const model = provider.resolveDynamicModel?.({
provider: "openai",
modelId: "gpt-5.4",
providerConfig: { api: "openai-codex-responses" },
providerConfig: { api: "openai-chatgpt-responses" },
modelRegistry: {
find: () => ({
provider: "openai",
@@ -154,7 +145,7 @@ describe("OpenAI provider Codex transport hooks", () => {
expect(model).toMatchObject({
provider: "openai",
id: "gpt-5.4",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
});
});
@@ -33,9 +33,9 @@ import {
OPENAI_CODEX_RESPONSES_BASE_URL,
} from "./base-url.js";
import { OPENAI_CODEX_DEFAULT_MODEL } from "./default-models.js";
import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js";
import { loginOpenAICodexDeviceCode } from "./openai-codex-device-code.js";
import { loginOpenAICodexOAuth } from "./openai-codex-oauth.runtime.js";
import { resolveCodexAuthIdentity } from "./openai-chatgpt-auth-identity.js";
import { loginOpenAICodexDeviceCode } from "./openai-chatgpt-device-code.js";
import { loginOpenAICodexOAuth } from "./openai-chatgpt-oauth.runtime.js";
import {
buildOpenAIResponsesProviderHooks,
buildOpenAISyntheticCatalogEntry,
@@ -46,7 +46,6 @@ import {
import { resolveOpenAICodexThinkingProfile } from "./thinking-policy.js";
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;
@@ -109,7 +108,7 @@ const OPENAI_CODEX_MODERN_MODEL_IDS = [
function isOpenAIOrLegacyCodexProvider(provider: string | undefined): boolean {
const normalized = normalizeProviderId(provider ?? "");
return normalized === PROVIDER_ID || normalized === LEGACY_OPENAI_CODEX_PROVIDER_ID;
return normalized === PROVIDER_ID;
}
function isLegacyCodexCompatBaseUrl(baseUrl?: string): boolean {
@@ -132,10 +131,12 @@ function normalizeCodexTransportFields(params: {
const api =
useCodexTransport &&
(!params.api || params.api === "openai-responses" || params.api === "openai-completions")
? "openai-codex-responses"
? "openai-chatgpt-responses"
: (params.api ?? undefined);
const baseUrl =
api === "openai-codex-responses" && useCodexTransport ? OPENAI_CODEX_BASE_URL : params.baseUrl;
api === "openai-chatgpt-responses" && useCodexTransport
? OPENAI_CODEX_BASE_URL
: params.baseUrl;
return { api, baseUrl };
}
@@ -225,7 +226,7 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
normalizeModelCompat({
id: trimmedModelId,
name: trimmedModelId,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
provider: PROVIDER_ID,
baseUrl: synthBaseUrl,
reasoning: true,
@@ -280,7 +281,7 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
}
patch = {
...patch,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: synthBaseUrl,
};
@@ -304,7 +305,7 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
lower === OPENAI_CODEX_GPT_54_LEGACY_MODEL_ID
? OPENAI_CODEX_GPT_54_MODEL_ID
: trimmedModelId,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
provider: PROVIDER_ID,
baseUrl: synthBaseUrl,
reasoning: true,
@@ -351,7 +352,7 @@ function withCodexTransport(
}
return normalizeModelCompat({
...model,
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl,
} as ProviderRuntimeModel);
}
@@ -381,7 +382,7 @@ function buildOpenAICodexAuthConfigPatch(): NonNullable<ProviderAuthResult["conf
async function refreshOpenAICodexOAuthCredential(cred: OAuthCredential) {
try {
const { refreshOpenAICodexToken } = await import("./openai-codex-provider.runtime.js");
const { refreshOpenAICodexToken } = await import("./openai-chatgpt-provider.runtime.js");
const refreshed = await refreshOpenAICodexToken(cred.refresh);
const identity = resolveCodexAuthIdentity({
accessToken: refreshed.access,
+17 -92
View File
@@ -8,7 +8,7 @@ const mocks = vi.hoisted(() => ({
openAIResponsesTransportStreamFn: vi.fn(),
}));
vi.mock("./openai-codex-provider.runtime.js", () => ({
vi.mock("./openai-chatgpt-provider.runtime.js", () => ({
refreshOpenAICodexToken: mocks.refreshOpenAICodexToken,
}));
@@ -59,7 +59,7 @@ function runWrappedPayloadCase(params: {
modelId: string;
model:
| Model<"openai-responses">
| Model<"openai-codex-responses">
| Model<"openai-chatgpt-responses">
| Model<"azure-openai-responses">;
extraParams?: Record<string, unknown>;
cfg?: Record<string, unknown>;
@@ -126,7 +126,7 @@ describe("buildOpenAIProvider", () => {
const provider = buildOpenAIProvider();
const apiKey = provider.auth.find((method) => method.id === "api-key");
expect(provider.hookAliases).toContain("openai-codex");
expect(provider.hookAliases).toEqual(["azure-openai", "azure-openai-responses"]);
expect(provider.catalog).toBeUndefined();
expectFields(apiKey?.wizard, {
choiceLabel: "OpenAI API Key",
@@ -142,7 +142,7 @@ describe("buildOpenAIProvider", () => {
const provider = buildOpenAICodexProviderPlugin();
expect(provider.id).toBe("openai");
expect(provider.hookAliases).toContain("openai-codex");
expect(provider.hookAliases).toEqual(["azure-openai", "azure-openai-responses"]);
});
it("prefers auth-aware Codex runtime metadata over static OpenAI catalog rows", () => {
@@ -161,20 +161,20 @@ describe("buildOpenAIProvider", () => {
expect(
provider.normalizeTransport?.({
provider: "openai-codex",
provider: "openai",
api: "openai-responses",
baseUrl: "https://chatgpt.com/backend-api",
} as never),
).toEqual({
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
});
expect(
provider.normalizeResolvedModel?.({
provider: "openai-codex",
provider: "openai",
modelId: "gpt-5.4",
model: {
provider: "openai-codex",
provider: "openai",
id: "gpt-5.4-codex",
name: "gpt-5.4-codex",
api: "openai-responses",
@@ -184,7 +184,7 @@ describe("buildOpenAIProvider", () => {
).toMatchObject({
id: "gpt-5.4",
name: "gpt-5.4",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
input: ["text", "image"],
});
@@ -371,7 +371,7 @@ describe("buildOpenAIProvider", () => {
expectFields(codexModel, {
provider: "openai",
id: "gpt-5.4",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
contextWindow: 1_050_000,
maxTokens: 128_000,
@@ -379,7 +379,7 @@ describe("buildOpenAIProvider", () => {
expectFields(selectedOauthModel, {
provider: "openai",
id: "gpt-5.4",
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
contextWindow: 1_050_000,
maxTokens: 128_000,
@@ -516,13 +516,13 @@ describe("buildOpenAIProvider", () => {
});
});
it("keeps legacy OpenAI Codex refs on the Codex thinking policy", () => {
it("keeps Codex-family OpenAI models on the Codex thinking policy", () => {
const provider = buildOpenAIProvider();
expect(
provider
.resolveThinkingProfile?.({
provider: "openai-codex",
provider: "openai",
modelId: "gpt-5.3-codex-spark",
} as never)
?.levels.map((level) => level.id),
@@ -531,7 +531,7 @@ describe("buildOpenAIProvider", () => {
provider
.resolveThinkingProfile?.({
provider: "openai",
modelId: "gpt-5.3-codex-spark",
modelId: "gpt-5.3",
} as never)
?.levels.map((level) => level.id),
).not.toContain("xhigh");
@@ -661,7 +661,7 @@ describe("buildOpenAIProvider", () => {
expect(
codexProvider.buildReplayPolicy?.({
provider: "openai",
modelApi: "openai-codex-responses",
modelApi: "openai-chatgpt-responses",
modelId: "gpt-5.4",
} as never),
).toEqual({
@@ -921,11 +921,11 @@ describe("buildOpenAIProvider", () => {
provider: "openai",
modelId: "gpt-5.4",
model: {
api: "openai-codex-responses",
api: "openai-chatgpt-responses",
provider: "openai",
id: "gpt-5.4",
baseUrl: "https://chatgpt.com/backend-api/codex/responses",
} as Model<"openai-codex-responses">,
} as Model<"openai-chatgpt-responses">,
extraParams: { effort: "high" },
}),
).toEqual({
@@ -983,81 +983,6 @@ describe("buildOpenAIProvider", () => {
expect(result.payload.reasoning).toEqual({ effort: "none" });
});
it("owns Codex wrapper composition for responses payloads", () => {
const provider = buildOpenAIProvider();
const wrap = provider.wrapStreamFn;
expect(wrap).toBeTypeOf("function");
if (!wrap) {
throw new Error("expected Codex wrapper");
}
const payload = {
store: false,
text: { verbosity: "medium" },
tools: [{ type: "function", name: "read" }],
};
mocks.openAIResponsesTransportStreamFn.mockImplementation((model, _context, options) => {
options?.onPayload?.(payload, model);
return {} as ReturnType<StreamFn>;
});
const result = runWrappedPayloadCase({
wrap,
provider: "openai",
modelId: "gpt-5.4",
extraParams: {
fastMode: true,
serviceTier: "priority",
text_verbosity: "high",
},
cfg: {
auth: {
profiles: {
"openai:default": {
provider: "openai",
mode: "oauth",
},
},
},
tools: {
web: {
search: {
enabled: true,
openaiCodex: {
enabled: true,
mode: "live",
allowedDomains: ["example.com"],
},
},
},
},
},
model: {
api: "openai-codex-responses",
provider: "openai",
id: "gpt-5.4",
baseUrl: "https://chatgpt.com/backend-api",
} as Model<"openai-codex-responses">,
payload,
});
expect(mocks.openAIResponsesTransportStreamFn).not.toHaveBeenCalled();
const headers = result.options?.headers as Record<string, unknown> | undefined;
expectFields(result.options?.headers, {
originator: "openclaw",
});
expect(typeof headers?.["User-Agent"]).toBe("string");
expect(String(headers?.["User-Agent"]).startsWith("openclaw/")).toBe(true);
expect(result.payload.store).toBe(false);
expect(result.payload.service_tier).toBe("priority");
expect(result.payload.text).toEqual({ verbosity: "high" });
expect(result.payload.tools).toEqual([
{ type: "function", name: "read" },
{
type: "web_search",
external_web_access: true,
filters: { allowed_domains: ["example.com"] },
},
]);
});
it("falls back to cached codex oauth credentials on accountId extraction failures", async () => {
const provider = buildOpenAIProvider();
const credential = {
+12 -45
View File
@@ -16,7 +16,7 @@ import { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "./default-models.js";
import {
buildOpenAIChatGPTAuthMethods,
buildOpenAICodexProviderHooks,
} from "./openai-codex-provider.js";
} from "./openai-chatgpt-provider.js";
import {
buildOpenAIResponsesProviderHooks,
buildOpenAISyntheticCatalogEntry,
@@ -24,13 +24,9 @@ import {
findCatalogTemplate,
matchesExactOrPrefix,
} from "./shared.js";
import {
resolveOpenAICodexThinkingProfile,
resolveOpenAIThinkingProfile,
} from "./thinking-policy.js";
import { resolveUnifiedOpenAIThinkingProfile } 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";
@@ -103,9 +99,9 @@ function shouldUseOpenAIResponsesTransport(params: {
return typeof params.baseUrl === "string" && isOpenAIApiBaseUrl(params.baseUrl);
}
function isOpenAIOrLegacyCodexProvider(provider: string | undefined): boolean {
function isOpenAIProvider(provider: string | undefined): boolean {
const normalized = normalizeProviderId(provider ?? "");
return normalized === PROVIDER_ID || normalized === LEGACY_OPENAI_CODEX_PROVIDER_ID;
return normalized === PROVIDER_ID;
}
function normalizeOpenAITransport(model: ProviderRuntimeModel): ProviderRuntimeModel {
@@ -130,10 +126,7 @@ function shouldUseCodexResponsesHooks(params: {
api?: ProviderRuntimeModel["api"] | null;
baseUrl?: string;
}): boolean {
if (normalizeProviderId(params.provider ?? "") === LEGACY_OPENAI_CODEX_PROVIDER_ID) {
return true;
}
if (params.api === "openai-codex-responses") {
if (params.api === "openai-chatgpt-responses") {
return true;
}
return typeof params.baseUrl === "string" && isOpenAICodexBaseUrl(params.baseUrl);
@@ -151,9 +144,6 @@ function resolveConfiguredAuthTransport(
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";
@@ -164,10 +154,7 @@ function resolveConfiguredAuthTransport(
const auth = ctx.config?.auth;
const profiles = auth?.profiles ?? {};
const orderedProfileIds = [
...(auth?.order?.[PROVIDER_ID] ?? []),
...(auth?.order?.[LEGACY_OPENAI_CODEX_PROVIDER_ID] ?? []),
];
const orderedProfileIds = auth?.order?.[PROVIDER_ID] ?? [];
for (const profileId of orderedProfileIds) {
const mode = profiles[profileId]?.mode;
if (mode === "oauth" || mode === "token") {
@@ -179,11 +166,7 @@ function resolveConfiguredAuthTransport(
}
const providerModes = Object.values(profiles)
.filter((profile) =>
[PROVIDER_ID, LEGACY_OPENAI_CODEX_PROVIDER_ID].includes(
normalizeProviderId(profile.provider),
),
)
.filter((profile) => normalizeProviderId(profile.provider) === PROVIDER_ID)
.map((profile) => profile.mode);
if (providerModes.some((mode) => mode === "oauth" || mode === "token")) {
return "codex";
@@ -194,10 +177,6 @@ function resolveConfiguredAuthTransport(
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({
@@ -335,15 +314,9 @@ export function buildOpenAIProvider(): ProviderPlugin {
return {
id: PROVIDER_ID,
label: "OpenAI",
hookAliases: ["openai-codex", "azure-openai", "azure-openai-responses"],
hookAliases: ["azure-openai", "azure-openai-responses"],
docsPath: "/providers/models",
envVars: ["OPENAI_API_KEY"],
oauthProfileIdRepairs: [
{
legacyProfileId: "openai-codex:default",
promptLabel: "OpenAI",
},
],
auth: [
...buildOpenAIChatGPTAuthMethods(),
createProviderApiKeyAuthMethod({
@@ -374,7 +347,7 @@ export function buildOpenAIProvider(): ProviderPlugin {
: resolveOpenAIGptForwardCompatModel(ctx),
preferRuntimeResolvedModel: (ctx) => codexHooks.preferRuntimeResolvedModel?.(ctx) ?? false,
normalizeResolvedModel: (ctx) => {
if (!isOpenAIOrLegacyCodexProvider(ctx.provider)) {
if (!isOpenAIProvider(ctx.provider)) {
return undefined;
}
if (
@@ -420,16 +393,10 @@ export function buildOpenAIProvider(): ProviderPlugin {
/content_filter.*(?:prompt|input).*(?:too long|exceed)/i.test(errorMessage),
resolveReasoningOutputMode: () => "native",
resolveThinkingProfile: ({ provider, modelId }) =>
normalizeProviderId(provider) === LEGACY_OPENAI_CODEX_PROVIDER_ID
? resolveOpenAICodexThinkingProfile(modelId)
: resolveOpenAIThinkingProfile(modelId),
normalizeProviderId(provider) === PROVIDER_ID
? resolveUnifiedOpenAIThinkingProfile(modelId)
: null,
isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_MODERN_MODEL_IDS),
buildMissingAuthMessage: (ctx) => {
if (ctx.provider !== PROVIDER_ID || ctx.listProfileIds("openai-codex").length === 0) {
return undefined;
}
return 'No API key found for provider "openai". You are authenticated with OpenAI Codex OAuth; OpenAI agent model runs use openai/gpt-* through the Codex runtime. Set OPENAI_API_KEY only for direct OpenAI API-key surfaces.';
},
augmentModelCatalog: (ctx) => {
const openAiGpt55ProTemplate = findCatalogTemplate({
entries: ctx.entries,
+1 -7
View File
@@ -8,16 +8,13 @@
"modelSupport": {
"modelPrefixes": ["gpt-", "o1", "o3", "o4"]
},
"providerAuthAliases": {
"openai-codex": "openai"
},
"providerEndpoints": [
{
"endpointClass": "openai-public",
"hosts": ["api.openai.com"]
},
{
"endpointClass": "openai-codex",
"endpointClass": "openai",
"hosts": ["chatgpt.com"]
},
{
@@ -239,7 +236,6 @@
"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,
@@ -252,7 +248,6 @@
"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,
@@ -265,7 +260,6 @@
"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,
+12 -4
View File
@@ -29,6 +29,7 @@ const manifest = JSON.parse(
setup?: {
providers?: Array<{ id: string }>;
};
providerEndpoints?: Array<{ endpointClass?: string; hosts?: string[] }>;
providerAuthAliases?: Record<string, string>;
};
@@ -95,17 +96,24 @@ describe("OpenAI plugin manifest", () => {
expect(packageJson.dependencies?.ws).toBe("8.21.0");
});
it("keeps removed Codex CLI import auth choice as a deprecated OpenAI login alias", () => {
it("exposes only current OpenAI login choices", () => {
const openAiLogin = manifest.providerAuthChoices?.find(
(choice) => choice.choiceId === "openai",
);
expect(openAiLogin?.deprecatedChoiceIds).toContain("openai-codex-import");
expect(openAiLogin?.deprecatedChoiceIds).toBeUndefined();
});
it("keeps legacy OpenAI Codex setup lookup routed to the OpenAI setup runtime", () => {
it("routes setup through the OpenAI setup runtime", () => {
expect(manifest.setup?.providers?.map((provider) => provider.id)).toEqual(["openai"]);
expect(manifest.providerAuthAliases?.["openai-codex"]).toBe("openai");
expect(manifest.providerAuthAliases).toBeUndefined();
});
it("classifies ChatGPT backend traffic with the supported OpenAI endpoint class", () => {
const chatGptEndpoint = manifest.providerEndpoints?.find((endpoint) =>
endpoint.hosts?.includes("chatgpt.com"),
);
expect(chatGptEndpoint?.endpointClass).toBe("openai");
});
it("keeps OpenAI media-understanding manifest metadata aligned with runtime audio support", () => {
@@ -3,7 +3,7 @@ import { vi } from "vitest";
const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn());
vi.mock("./openai-codex-oauth.runtime.js", () => ({
vi.mock("./openai-chatgpt-oauth.runtime.js", () => ({
loginOpenAICodexOAuth: loginOpenAICodexOAuthMock,
}));
+1 -7
View File
@@ -14,15 +14,9 @@ export function createOpenAIProvider(): ProviderPlugin {
return {
id: "openai",
label: "OpenAI",
hookAliases: ["openai-codex", "azure-openai", "azure-openai-responses"],
hookAliases: ["azure-openai", "azure-openai-responses"],
docsPath: "/providers/models",
envVars: ["OPENAI_API_KEY"],
oauthProfileIdRepairs: [
{
legacyProfileId: "openai-codex:default",
promptLabel: "OpenAI",
},
],
auth: [
{
id: "oauth",
@@ -2,17 +2,22 @@ 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", () => {
it("keeps OpenAI thinking policy for openai refs", () => {
const codexProfile = resolveThinkingProfile({
provider: "openai-codex",
provider: "openai",
modelId: "gpt-5.3-codex-spark",
});
const openaiProfile = resolveThinkingProfile({
provider: "openai",
modelId: "gpt-5.3-codex-spark",
modelId: "gpt-5.3",
});
const openaiMiniProfile = resolveThinkingProfile({
provider: "openai",
modelId: "gpt-5.4-mini",
});
expect(codexProfile?.levels.map((level) => level.id)).toContain("xhigh");
expect(openaiProfile?.levels.map((level) => level.id)).not.toContain("xhigh");
expect(openaiMiniProfile?.levels.map((level) => level.id)).toContain("xhigh");
});
});
+2 -7
View File
@@ -1,8 +1,5 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import {
resolveOpenAICodexThinkingProfile,
resolveOpenAIThinkingProfile,
} from "./thinking-policy.js";
import { resolveUnifiedOpenAIThinkingProfile } from "./thinking-policy.js";
export function normalizeConfig(params: { provider: string; providerConfig: ModelProviderConfig }) {
return params.providerConfig;
@@ -11,9 +8,7 @@ export function normalizeConfig(params: { provider: string; providerConfig: Mode
export function resolveThinkingProfile(params: { provider: string; modelId: string }) {
switch (params.provider.trim().toLowerCase()) {
case "openai":
return resolveOpenAIThinkingProfile(params.modelId);
case "openai-codex":
return resolveOpenAICodexThinkingProfile(params.modelId);
return resolveUnifiedOpenAIThinkingProfile(params.modelId);
default:
return null;
}
@@ -204,12 +204,12 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
it("treats a Codex OAuth profile as configured when no API key is present", () => {
const provider = buildOpenAIRealtimeTranscriptionProvider();
const cfg = { auth: { order: { "openai-codex": ["openai-codex:default"] } } };
const cfg = { auth: { order: { openai: ["openai:default"] } } };
providerAuthMocks.isProviderAuthProfileConfigured.mockReturnValue(true);
expect(provider.isConfigured({ cfg: cfg as never, providerConfig: {} })).toBe(true);
expect(providerAuthMocks.isProviderAuthProfileConfigured).toHaveBeenCalledWith({
provider: "openai-codex",
provider: "openai",
cfg,
});
});
@@ -222,7 +222,7 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
response: new Response(JSON.stringify({ value: "ek-test" }), { status: 200 }),
release,
});
const cfg = { auth: { order: { "openai-codex": ["openai-codex:default"] } } };
const cfg = { auth: { order: { openai: ["openai:default"] } } };
const session = provider.createSession({
cfg: cfg as never,
providerConfig: {},
@@ -233,7 +233,7 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
expect(socket.headers?.Authorization).toBe("Bearer ek-test");
expect(providerAuthMocks.resolveProviderAuthProfileApiKey).toHaveBeenCalledWith({
provider: "openai-codex",
provider: "openai",
cfg,
});
const request = mockCallArg(ssrfMocks.fetchWithSsrFGuard);
@@ -300,7 +300,7 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
bridge.close();
expect(resolveProviderAuthProfileApiKeyMock).toHaveBeenCalledWith({
provider: "openai-codex",
provider: "openai",
cfg: {},
includeExternalCliAuth: true,
});
@@ -588,7 +588,7 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
expect(provider.isConfigured({ cfg, providerConfig: {} })).toBe(true);
expect(isProviderAuthProfileConfiguredMock).toHaveBeenCalledWith({
provider: "openai-codex",
provider: "openai",
cfg,
includeExternalCliAuth: true,
});
@@ -632,7 +632,7 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
});
expect(resolveProviderAuthProfileApiKeyMock).toHaveBeenCalledWith({
provider: "openai-codex",
provider: "openai",
cfg,
includeExternalCliAuth: true,
});
@@ -664,7 +664,7 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
});
expect(resolveProviderAuthProfileApiKeyMock).toHaveBeenCalledWith({
provider: "openai-codex",
provider: "openai",
cfg,
includeExternalCliAuth: true,
});
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
const RESPONSES_FAMILY_APIS = new Set([
"openai-responses",
"openai-codex-responses",
"openai-chatgpt-responses",
"azure-openai-responses",
]);
+1 -1
View File
@@ -12,7 +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(provider.aliases).toEqual(["openai"]);
expect(authMethodIds(provider)).toEqual(["oauth", "device-code", "api-key"]);
expect(oauth?.label).toBe("ChatGPT Login");
expect(oauth?.wizard?.choiceId).toBe("openai");
-1
View File
@@ -73,7 +73,6 @@ export function buildOpenAISetupProvider(): ProviderPlugin {
return {
id: "openai",
aliases: ["openai-codex"],
label: "OpenAI",
docsPath: "/providers/models",
envVars: ["OPENAI_API_KEY"],
@@ -103,7 +103,6 @@ export function describeOpenAIProviderCatalogContract() {
switch (params.provider) {
case "azure-openai-responses":
case "openai":
case "openai-codex":
return ["openai"];
default:
return undefined;

Some files were not shown because too many files have changed in this diff Show More