
Codex Orchestration
- 575 repo stars
- Updated August 1, 2026
- Cjbuilds/Codex-Orchestration
An open-source Codex plugin that assigns different AI models to roles (advisor, executor, researcher, reviewer) and routes each workflow step to the best-suited model, with the root task model orchestrating.
About
codex-orchestration is an open-source Codex plugin by Cjbuilds that adds multi-model role routing to Codex's existing multi-agent flow. You assign compatible models to roles such as advisor, executor, researcher, reviewer, writer, or supervisor, then describe a workflow - for example Claude Fable 5 critiques the plan while GPT-5.6 executors implement independent slices - and the root task model orchestrates every handoff, integrates results, and verifies the outcome. It supports one-time native setup, per-task overrides, custom-agent TOML creation at project or personal scope, and a disable command that restores pre-setup routing.
- Bring any model into Codex (Claude Fable 5, GPT-5.6, etc.)
- Assign models to roles: advisor, executor, researcher, reviewer
- Compose custom workflows (one model plans, another implements)
- Creates native Codex custom-agent TOML files
- Root task model stays the orchestrator - no second scheduler
Codex Orchestration by the numbers
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/Cjbuilds/Codex-Orchestration --skill codex-orchestrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| repo stars | ★ 575 |
|---|---|
| Last updated | August 1, 2026 |
| Repository | Cjbuilds/Codex-Orchestration ↗ |
What it does
Assign models to roles (advisor, executor, researcher, reviewer) inside Codex and run multi-model workflows where each step executes on the model best suited for it.
Who is it for?
Codex users who want Claude Fable 5, GPT-5.6, or other models collaborating in one task - one model planning or critiquing while another implements.
What you get
Multi-model Codex workflows with per-role model routing - the author reports fewer rate-limit hits and faster implementation than single-model runs.
Files
Codex Orchestration
The model selected when this Codex task started is already the orchestrator. Never ask the user to configure another one and never change the root model on this skill's behalf.
This skill adds a model route to Codex's existing multi-agent flow. It does not create another scheduler.
Understand the command
Support these simple forms:
/codex-orchestration setup executor: GPT-5.6 Luna Extra High
/codex-orchestration setup executor: GPT-5.6 Luna Extra High, advisor: Claude Fable 5 Extra High
/codex-orchestration create project role: researcher
/codex-orchestration create personal roles: researcher, writer, reviewer
/codex-orchestration status
/codex-orchestration disable
/codex-orchestration remove custom roles personally
/codex-orchestration executor: GPT-5.6 Terra high — <one task only>setup installs or updates the personal one-time routing policy. create project role or create personal role creates native Codex custom-agent files. status inspects built-in routing. disable restores its pre-setup values.
remove custom roles cleans only verified plugin-managed advisor/executor files. Arbitrary native roles are user-owned. An invocation with seats and work but no control verb is a current-task override and must not rewrite config.
The executor is required for setup or a task-local override. It is not required for a custom-role creation request. The advisor is optional: if omitted, it means advisor: none. Do not ask a separate advisor question unless the user asks for help choosing one.
If the executor is missing, ask only:
Which executor model and effort should Codex use? You can optionally include an advisor; omission means none.Because explicit skills may not reload from a bare reply, include a ready-to-copy line using the exact label shown by the client and preserve the original work:
<exact-skill-label> setup executor=<model>@<effort-or-auto>, advisor=<model>@<effort-or-auto>|noneFor a task-local request, append — <original task>. Keep every supplied modifier. Do not lose the user's task while collecting a model choice.
If an old prompt contains orchestrator:, explain that the current task model already owns that role. Ignore that seat instead of switching or persisting it.
Normalize Extra High to xhigh for Codex models. Claude Fable 5 Extra High is the built-in advisor label; map it to --advisor-fable --advisor-effort max, not the Codex model catalog. Resolve every other display name to an exact ID only through the executing host's model catalog, picker, a loaded custom agent, or official provider documentation. Never invent an ID. For persistent direct routing, resolve auto to the catalog's concrete default.
Read providers-and-models.md before setup, when clients disagree, when a model is absent, when providers differ, or when custom agents or legacy migration are involved.
Create arbitrary custom roles
Use native Codex custom-agent files for roles beyond the built-in advisor and executor seats. Examples include researcher, reviewer, writer, supervisor, security auditor, browser debugger, or domain expert.
Use project scope when the user says project, repo, workspace, or current project. Write to <trusted-project>/.codex/agents/<role-name>.toml. Use personal scope only when explicitly requested and write to ~/.codex/agents/<role-name>.toml.
Before writing:
1. Normalize the role name to lowercase snake case and validate ^[a-z][a-z0-9_]{0,62}$. 2. Require a clear purpose and developer_instructions that keep the role bounded. 3. Resolve the model and effort from the active catalog or a user-confirmed exact ID. 4. If model_provider is supplied, require an existing configured and authenticated compatible provider. Never create provider access or collect credentials. 5. Use the current task permission mode by default. Add sandbox_mode only when the user requests it. A role may request a narrower sandbox; it never bypasses the parent task's authority. 6. Keep agents.max_depth = 1 behavior unless the user explicitly asks for nested agents. A custom role should not create descendants by default. 7. Refuse symlinked paths, duplicate agent names, malformed TOML, and overwriting an existing file without explicit replacement approval.
A custom agent file must define name, description, and developer_instructions. It may also define model, model_reasoning_effort, model_provider, sandbox_mode, mcp_servers, and skills.config when supported.
Preview the path and complete TOML before writing. A literal create request authorizes a clean new file after preview. Replacing or deleting an existing user-owned role requires a separate explicit decision.
Do not add the plugin ownership marker to arbitrary roles. Do not claim disable or remove custom roles will remove them. Tell the user to start a new task after creation so Codex loads the new roles.
When the user supplies a sequence such as researcher -> reviewer -> writer, preserve it as task-level workflow instructions. The root orchestrator owns every handoff, resolves conflicting feedback, verifies the result, and may skip only optional steps.
If the user combines a workflow with a Codex Goal, leave Goal lifecycle and limits under Codex's normal Goal controls. The orchestration policy operates inside the Goal; this skill does not silently create, pause, resume, or clear it.
One-time native setup
Use this path for a current same-provider setup such as Sol root to Luna or Terra executors. Claude Fable 5 is the one built-in cross-provider advisor exception because it runs through the bundled read-only MCP bridge and the user's authenticated Claude Code CLI.
1. Identify the Codex binary used by the active host. Do not assume the shell codex is the Desktop binary. 2. Resolve the exact executor and optional advisor IDs and efforts from that host. 3. Run the bundled native configurator from this skill's real directory with Python 3.11 or newer. Use python3 on typical macOS/Linux hosts; on Windows select an available py -3.11 or python launcher after checking its version. Never use a repository-relative copy from the user's workspace. 4. Inspect the dry-run output. A literal setup request authorizes applying a clean, non-replacement personal policy after that preview. 5. Start a new task after apply. The user chooses the orchestrator in the normal model picker and no longer needs to invoke this skill for ordinary work.
Typical dry run and apply:
python3 <skill-dir>/scripts/configure_native_routing.py \
--codex-bin <active-codex-binary> \
--executor-model gpt-5.6-luna \
--executor-effort xhigh
python3 <skill-dir>/scripts/configure_native_routing.py \
--codex-bin <active-codex-binary> \
--executor-model gpt-5.6-luna \
--executor-effort xhigh \
--applyAdd --advisor-model and --advisor-effort for a same-provider Codex advisor. For Claude Fable 5, use --advisor-fable --advisor-effort max. The configurator requires Claude Code to be logged in through a first-party Pro or Max account, chooses an available Python 3.11+ MCP launcher, and performs only an auth/capability check during setup. It never extracts a token, writes a credential, or makes a model call during setup or status. Omission persists advisor: none.
The configurator capability-tests the complete four-field preset on the active target, codex on PATH when different, the known macOS Desktop binary when present, and every explicit --compat-bin. A successful isolated config probe means that client can parse the preset; it is not a live child-model confirmation. Report route accepted or used and confirmed only from the exact live spawn evidence defined below. Ask about other Codex/IDE installations that share this config only when the environment suggests they exist, and pass their binaries explicitly. If the request or active host indicates a named --profile, explain that normal setup manages the default user layer and is not verified for that profile; do not add a routine question for users with no profile signal. If a checked client rejects any managed field, stop before apply. Recommend updating it or using the task-local fallback. --allow-incompatible-client requires a separate explicit user decision because it can make the shared config unreadable to that client.
For the current validated v2 direct route, set tool_namespace = "agents". Live testing on Desktop 0.144.0-alpha.4 showed that the default reserved collaboration.spawn_agent schema rejected expanded model/effort metadata, while agents accepted the same request and spawned Luna at xhigh. Treat this as a required control-surface setting for that tested path, not as the executor selection. usage_hint_text carries the actual executor/advisor route.
Do not add enabled = true for a Sol or Terra root. Their current model metadata selects v2. The configurator intentionally manages these routing fields:
features.multi_agent_v2.hide_spawn_agent_metadata;features.multi_agent_v2.tool_namespace;features.multi_agent_v2.multi_agent_mode_hint_text;features.multi_agent_v2.usage_hint_text.
When Claude Fable 5 is selected, it additionally manages only the plugin-scoped enabled override for the chosen bundled MCP launcher and any launcher variant already overridden by the user. All bundled variants are disabled by default. The original override values are stored and restored by disable. Codex's TOML editor may retain an inert empty table header after deleting the last override; never rewrite the file merely to remove that cosmetic header.
It uses Codex App Server's config/read and config/batchWrite APIs, not a home-grown TOML rewrite. It preserves unrelated settings and comments, validates the whole effective config, and uses the user-layer version to detect races. Restore snapshots cover the four routing fields plus the narrowly scoped MCP overrides only when Fable is selected; the namespaced state also records schema/version markers, config path, selected seats, and scalar-conversion metadata when needed. If the user explicitly replaces existing hint text, the exact prior text is stored for restoration; warn them never to place credentials in routing hints.
If a user-authored mode or usage hint already exists, do not replace it automatically. Show the conflict. Use --replace-existing-policy only after the user explicitly approves replacing and later restoring those exact values.
Status, change, and disable
For status:
python3 <skill-dir>/scripts/configure_native_routing.py \
--codex-bin <active-codex-binary> \
--status
python3 <skill-dir>/scripts/configure_native_routing.py \
--codex-bin <active-codex-binary> \
--status --require-effectiveRun status from the target project. The first form is descriptive. Use --require-effective for automation and release gates; it returns nonzero for incompatible clients, conflicts, overrides, incomplete controls, unavailable agent routes, or orphaned v0.4+ personal roles. Report the current task model as the orchestrator, the configured executor and advisor, whether the personal policy is installed and effective in that workspace, whether effective spawn controls are visible, whether the effective tool namespace is agents, the target config path, and checked-client compatibility. State that neither status form proves a live route or infers v2 activation for the model selected in a task; current Sol or Terra is the intended root.
To change seats, run normal setup again. The configurator keeps the original restore snapshot rather than treating its own managed values as user settings.
For disable, dry-run and then apply. A literal disable request authorizes a clean restore:
python3 <skill-dir>/scripts/configure_native_routing.py \
--codex-bin <active-codex-binary> \
--disable
python3 <skill-dir>/scripts/configure_native_routing.py \
--codex-bin <active-codex-binary> \
--disable --applyDisable must remain available even if an older client is incompatible with the active policy. Refuse to erase managed fields that the user edited after setup; explain the conflict instead.
For personal v0.4 custom roles, preview and apply removal with configure_orchestration.py --scope personal --personal-route-names --remove-saved-roles. For older fixed-name personal roles, run a separate preview without --personal-route-names. Project removal uses --scope project --root <trusted-project> --remove-saved-roles. Delete only files that the configurator fully validates as managed; edited or user-owned files require manual review.
Claude Fable 5 advisor
Use this built-in route when the user names Claude Fable 5. Do not create a custom provider or custom-agent file for it.
In every user-facing status or result, use the exact name Claude Fable 5. Report authentication as first-party login ready; do not expose or restate Claude account-plan metadata.
Prerequisites:
- the official
claudeCLI is installed; claude auth statusreports a first-party Pro or Max login;- a Python 3.11+ launcher is available.
The plugin packages three disabled MCP launcher variants for macOS, Linux, and Windows. Setup enables exactly the compatible variant through the plugin's namespaced config. At review time the MCP server removes API-key and Bedrock/Vertex/Foundry override variables, re-checks first-party login, and invokes claude -p --model claude-fable-5 with --safe-mode, no tools, no session persistence, prompt suggestions disabled, and JSON output. The saved route pins the model and effort; the root cannot replace them through tool arguments.
The bridge accepts only one self-contained packet. It requires PLAN_APPROVED or PLAN_REVISE as the first non-empty line and requires runtime modelUsage to confirm claude-fable-5. Any auth, transport, format, or model-confirmation failure is advisor unavailable, never approval. It returns no account identifier or credential.
Durable or cross-provider custom agents
Direct model routing is same-provider. Except for the built-in Claude Fable 5 MCP route above, a different provider needs an already authenticated Codex-compatible provider and a loaded custom agent that pins model_provider.
Use the existing standalone-agent configurator for this extended path. Personal scope is required for machine-local provider IDs and affects all projects, so the user's explicit cross-provider setup request must name or confirm the existing provider ID. Never create provider definitions, collect keys in chat, or write credentials.
First preview and apply the namespaced custom agents:
python3 <skill-dir>/scripts/configure_orchestration.py \
--scope personal \
--personal-route-names \
--codex-bin <active-codex-binary> \
--executor-model <exact-id> \
--executor-effort <effort> \
--executor-provider <existing-provider-id> \
--advisor-model <exact-id> \
--advisor-effort <effort> \
--advisor-provider <existing-provider-id>When this cross-provider/custom-agent setup omits an advisor, pass --remove-advisor so a previously managed advisor is not left as a misleading saved seat. Apply only after a clean preview. Then point the native policy at the loaded role names:
python3 <skill-dir>/scripts/configure_native_routing.py \
--codex-bin <active-codex-binary> \
--executor-agent <reported-executor-agent-name> \
--advisor-agent <reported-advisor-agent-name> \
--applyOmit --advisor-agent when none is configured. --personal-route-names generates stable CODEX_HOME-specific names and prints them for the native command. The native configurator verifies exactly one matching personal file and refuses a same-name project role in the current workspace. A custom-agent file is a stronger durable model/provider pin than a direct tool hint, but runtime identity is confirmed only when the host exposes it. Start a new task so Codex loads the role files.
These are two separate storage transactions. If the native command fails after the role transaction applied, immediately preview and then apply:
python3 <skill-dir>/scripts/configure_orchestration.py \
--scope personal \
--personal-route-names \
--codex-bin <active-codex-binary> \
--remove-saved-rolesRemove only files the configurator validates as managed. If cleanup fails or the operation was interrupted, stop and run native --status --require-effective; report each orphaned managed role for manual review. Never claim the two stores changed atomically. On Windows, new managed roles can be created, but updating or removing an existing role fails closed; explain that limitation before choosing the custom-agent path.
The standalone configurator also retains project-scoped saved roles, safe removal, and opt-in migration for releases 0.1–0.3. It must never change the root model, permissions, credentials, or global agent limits.
Preserve Codex's decisions
The current task model remains the root. It owns intent, planning, architecture, decomposition, delegation, integration, review, final verification, and the final answer.
Codex decides whether a plan helps, whether any work is safely delegable, how many independent slices exist, and whether parallelism is worth its context and integration cost. Keep simple, tightly coupled, context-heavy, and root-owned work with the root.
This skill and its saved policy must never:
- create a second orchestrator;
- force a spawn or fixed worker count;
- create or change Goal state;
- weaken approvals or permissions;
- create nested executor teams;
- let an advisor direct executors;
- parallelize overlapping writes;
- silently substitute the root model for an unavailable child route.
An explicit no subagents instruction always wins. A current-task seat override wins over the saved default for that task only.
Spawn routed children correctly
Inspect the callable subagent interface. A saved current preset should expose the routed tool under agents; if only collaboration is exposed, do not assume the expanded direct route works. For a task-local fallback, use whichever callable namespace is actually present and pass exact route controls only when its schema exposes them.
Every spawn that supplies model, reasoning_effort, or agent_type through this skill must use:
fork_turns = "none"A small positive partial fork is technically valid in Codex, but this skill deliberately requires none: it minimizes duplicated context and makes the root send a deliberate self-contained packet. Never use the default all with a different route. Full-history forks inherit the root model and Codex rejects the override.
For a direct executor route, pass the exact configured model and concrete effort. For a custom route, pass the exact namespaced agent_type. Do not force a service tier; supported children may inherit Fast/priority from the parent, so tell users who prioritize allowance savings not to run the root in Fast mode.
Direct model overrides keep the root's provider. Before a direct spawn, establish that the target model is on the same provider. If it differs or cannot be established, mark the route unavailable and require a custom agent that pins model_provider.
After spawning, use the tool result or client metadata to confirm the accepted route. Distinguish:
native policy installed: the managed user policy exists; v2 activation still depends on the selected root and effective workspace config;pinned custom agent available: a matching role is loaded, but has not run;route accepted: the current tool accepted and validated the requested route controls;used and confirmed: use only when the client explicitly exposes effective runtime model/provider/effort metadata;inherited root — requested child model was not used;unavailable: the requested route cannot run here;none: no advisor is configured.
Tool acceptance proves the requested route was valid and accepted, not necessarily that the client exposes post-start runtime identity. Child prose claiming a model name is not proof. If an exact route fails, report it to the root. Continue root-owned work only when the user did not make delegation or that seat a hard requirement.
Advisor review
Use an advisor only when configured and the root has a non-trivial plan or executor slices worth reviewing. Skip it for simple work.
Before executor work, send one advisor a self-contained packet containing:
- user intent and acceptance criteria;
- relevant repository facts and constraints;
- the root's plan and proposed executor slices;
- dependencies, ownership, and sequencing;
- material risks and verification checks.
Tell the advisor to review only, report only to the root, avoid edits and mutation, never spawn, and never contact executors. Require exactly one first-line signal:
PLAN_APPROVED
PLAN_REVISEPLAN_APPROVED means no material gap was found in the supplied packet, not that success is guaranteed. PLAN_REVISE must give prioritized material gaps and a concrete correction for each. Style preferences do not justify revision.
The root adjudicates every suggestion and owns the revised plan. Allow at most one confirmation pass after a material revision. A configured advisor is a gate for a non-trivial executor plan by default. Transport failure, malformed output, inaccessible routing, or missing context means advisor unavailable, never approval; stop before executor work unless the user explicitly made the advisor best-effort.
For Claude Fable 5, call the configured MCP server's review_plan tool instead of spawning an advisor child. It remains root-only and read-only; executors never receive the tool or direct it.
Executor handoff
Give each executor one bounded packet with:
- objective and boundaries;
- only the context and repository facts it needs;
- owned files or explicit read-only scope;
- dependencies and stop conditions;
- acceptance criteria and smallest useful verification;
- required handoff format.
Require it to preserve unrelated work, stay inside the slice, avoid the advisor, avoid descendants, and report blockers rather than guess. The handoff includes status, work completed, files or evidence, checks run, and remaining risks.
Parallelize only genuinely independent slices with non-overlapping write ownership. The root inspects, integrates, and verifies every handoff. Executor completion is never final acceptance.
Task-local and older-client fallback
When the persistent policy is unavailable, apply the supplied seats only to the work in the same invocation. Do not claim that a mutable team was saved.
Use the strongest exact control the current client exposes:
1. a matching loaded namespaced custom agent; 2. accepted direct model and reasoning_effort inputs with fork_turns = "none"; 3. a clearly labeled prompt preference when exact routing is unavailable; 4. unavailable when the provider or model cannot be reached.
For task-local auto, omit the reasoning-effort input. Never pass the literal string auto to a spawn tool; the effective inherited or host-chosen effort remains unverified unless the client exposes it.
Report a compact activation status and continue the included task:
Codex Orchestration
Orchestrator: <active model or current task model> — active
Executor: <model>@<effort> — <route state>
Advisor: <model>@<effort> — <route state>, or none
Delegation: Codex decides when it helps; Plan and Goal behavior unchangedNever report a prompt preference or saved file as a model that actually ran. Report an exact tool call as route accepted; reserve runtime confirmation for explicit effective metadata.
Keep savings language honest
The purpose is to spend high-end capacity where judgment matters and use an efficient coding model for eligible execution volume. Do not create agents solely to hit a percentage.
The “about 65%” example is a model-weighted credit calculation: at the published Luna rate of 20% of Sol, a comparable token mix with 20% on Sol and 80% on Luna costs 0.20 + (0.80 × 0.20) = 0.36, about 64% fewer credits before orchestration overhead.
Never call that 65% fewer raw tokens, a guaranteed five-hour or weekly-limit saving, a fixed monetary saving, or five times more completed work. Advisor calls, duplicated context, retries, tools, Fast service tier, and unnecessary workers can reduce or erase the benefit.
Resources
scripts/configure_native_routing.py: one-time native setup, status, update, and disable.scripts/fable_advisor_mcp.py: fail-closed Claude Fable 5 plan-review bridge.scripts/configure_orchestration.py: namespaced custom agents, provider pins, safe removal, and legacy migration.scripts/inspect_models.py: fallible host-catalog diagnostics.- providers-and-models.md: detailed capability, provider, compatibility, persistence, and usage boundaries.
interface:
display_name: "Codex Orchestration"
short_description: "Build multi-model Codex workflows"
default_prompt: "Use $codex-orchestration to assign compatible models to roles and configure my workflow."
policy:
allow_implicit_invocation: false
Models, Providers, and Routing Boundaries
Use this reference for setup, client compatibility, custom providers, or a route that does not behave as requested.
The shortest correct model
1. The model selected for the Codex task is the root orchestrator. 2. A current Sol or Terra root uses multi-agent v2. 3. The saved policy tells the root which exact route to request for every delegated executor and optional advisor. 4. Codex still decides whether a spawn helps. 5. Every different-model, different-effort, or custom-agent child uses fork_turns = "none" and a self-contained packet. 6. The root reviews and verifies all child work.
The policy tells Codex which route to request for delegated executor work. It does not force every task to delegate or add a second scheduler.
Current capability matrix
These facts were source-checked and runtime-tested on July 10, 2026. Always capability-test the actual host because the fields are still evolving.
| Capability | Current behavior | Consequence |
|---|---|---|
| Sol model metadata | multi_agent_version = v2 | A Sol root uses v2 without forcing the feature flag. |
| Terra model metadata | multi_agent_version = v2 | A Terra root also uses the native policy. |
| Luna model metadata | multi_agent_version = v1 | Luna is suitable as a v2 child, but a Luna root does not activate this v2 policy. |
hide_spawn_agent_metadata = false | Shows agent_type, model, reasoning_effort, and service_tier on v2 spawn | Required for direct route control; it does not select a route alone. |
tool_namespace = "agents" | On live-tested Desktop 0.144.0-alpha.4, the default collaboration namespace rejected expanded model/effort metadata; agents accepted it and spawned Luna at xhigh. | Required for this validated direct-routing path. It changes the callable namespace but does not select Luna. |
usage_hint_text | Appended to the spawn tool description | Carries the exact executor/advisor route where the root chooses children. |
multi_agent_mode_hint_text | Replaces the default proactive/explicit mode hint and is sent to root and child tasks | Must contain both root and child boundaries. |
| Claude Fable 5 MCP route | Root-only review_plan tool invokes the authenticated Claude Code CLI headlessly | Built-in cross-provider advisor exception; no custom Codex provider or advisor child. |
fork_turns default | all | Different model/effort/role overrides are rejected unless the call uses none or a positive partial fork. |
| Effective concurrency | Determined by the active Codex version and agents.max_threads configuration | This plugin never changes the limit or forces a worker count. |
| Older CLI 0.142.5 | Rejects multi_agent_mode_hint_text as an unknown feature-table field | Never write the global native policy without checking every known shared-config client. |
The installer does not infer this from version strings. It launches each detected binary with an isolated CODEX_HOME and probes whether it can parse all four managed fields. That is a config-compatibility check, not proof of a live child route.
Why enabled = true is omitted
Current resolution prefers the selected model's multi_agent_version over the global feature flag. Sol and Terra already select v2, so the one-time setup does not need to force it.
Forcing features.multi_agent_v2.enabled = true can:
- show an under-development feature warning;
- conflict with an older
agents.max_threadssetting; - change behavior for unrelated root models without the user asking.
If the user's config uses the older scalar form multi_agent_v2 = true|false, the configurator temporarily converts that value to the equivalent table form and records the original scalar. Disable restores the exact boolean only if no other table fields were added afterward.
What the four managed fields do
The control surface and the route are separate:
hide_spawn_agent_metadata = falseexposes the model, effort, agent-type, and service-tier spawn inputs;tool_namespace = "agents"makes the expanded route callable on the currently validated Desktop build;multi_agent_mode_hint_textcarries the root/child behavior and safety boundaries;usage_hint_textcarries the exact executor and optional advisor route.
multi_agent_mode_hint_text describes the policy:
- current task model is the one root orchestrator;
- Codex decides whether delegation is useful;
- optional advisor is root-only and reviews before executor work;
- executor packets are bounded and self-contained;
- children do not create descendants;
- user overrides and
no subagentswin; - Goal, permissions, approvals, and worker counts are not changed.
usage_hint_text attaches the route to the spawn tool itself:
executor -> model="gpt-5.6-luna", reasoning_effort="xhigh", fork_turns="none"
advisor -> model="gpt-5.6-terra", reasoning_effort="high", fork_turns="none"For a durable custom-agent route it uses:
agent_type="codex_orchestration_executor", fork_turns="none"
agent_type="codex_orchestration_advisor", fork_turns="none"For Claude Fable 5 it names the enabled bundled MCP server and tells the root to call its review_plan tool. This is a root tool call, not spawn_agent, so fork_turns does not apply.
The custom mode text is visible in spawned children too. That is why it says: if root, orchestrate; if child, stay within the packet and never spawn.
Routing strength and its honest boundary
There is no global Codex field named executor_model. The native same-provider route combines:
- visible v2 spawn metadata under the validated
agentsnamespace; - persistent spawn-tool guidance;
- a model-visible exact
modelandreasoning_effortinput; - runtime catalog validation when the tool call is accepted;
- optional effective-runtime confirmation when the client exposes it.
That is strong routing, but it is not a separate engine-level scheduler. The root can still choose not to delegate. Tool acceptance proves Codex accepted and validated the requested route; it does not guarantee that every client exposes the effective post-start identity. If the model ignores the required route or the tool rejects it, report that mismatch rather than claiming success.
Setup runs before a future task chooses its root, so it cannot persist a mechanically verified future root-provider identity. Direct routes are valid only when the active task can establish that the requested model belongs to the inherited root provider. If provider identity is missing or ambiguous, fail closed and use a provider-pinned custom agent.
A custom-agent file is the stronger persistent pin for a reusable role because the role config can set model, model_reasoning_effort, and model_provider. A stronger live parent override can still win, so confirm the effective child metadata either way.
Forking rules
V2 spawn_agent defaults to a full-history fork. Full-history children inherit the root model, provider, and reasoning effort. Codex therefore rejects agent_type, model, or reasoning_effort on a fork with fork_turns = "all".
Use:
fork_turns = "none"and send a self-contained task packet. A small positive turn count also permits overrides, but none is the Codex-Orchestration default because it minimizes duplicate context and makes the handoff deliberate.
Correctness wins over context savings. If a bounded packet cannot carry the necessary context safely, keep the work with the root instead of forcing a cheaper child.
Start with the executing host
Do not keep a static display-name alias table. Model IDs, efforts, access, providers, and model metadata change.
Resolve seats in this order:
1. active host's App Server model/list result; 2. current client model picker or accepted spawn controls; 3. a loaded namespaced custom agent; 4. exact binary catalog diagnostics; 5. official provider documentation; 6. user-supplied exact ID when the sources are ambiguous.
scripts/inspect_models.py and debug catalog commands are useful signals, not permanent APIs. A missing shell-CLI model does not prove a newer Desktop model is unavailable. Always report which binary and catalog supplied the model IDs for a persistent preset; do not call that a live route confirmation.
For task-local auto, omit the effort override and call the effective effort unverified until exposed. For persistent direct or custom-agent routing, resolve auto to the model's concrete catalog default so the root effort cannot leak into the child.
Native persistence and restoration
configure_native_routing.py writes the personal user config because the policy is meant to work in later tasks and projects.
It uses the official App Server flow:
initialize -> initialized -> config/read(includeLayers=true)
-> config/batchWrite(expectedVersion=...)
-> config/read verificationThe App Server permits writes only to the user config. It performs full schema and managed-requirement validation, preserves TOML comments and unrelated fields through toml_edit, atomically persists the file, returns okOverridden when a higher layer wins, and rejects a stale user-layer version.
The configurator writes each owned nested field separately, except when converting a legacy boolean feature shape. It refuses to replace user-authored hint strings unless --replace-existing-policy was explicitly approved. Setup verifies both the user layer and the effective config in the current workspace; it rolls back when a project or managed layer already overrides the installed policy there.
Restore state lives at:
~/.codex/.codex-orchestration-routing.jsonIt contains the prior and managed values of the four routing fields, chosen seat IDs, schema/version markers, scalar-conversion metadata when needed, and config path. When Claude Fable 5 is selected, it also records only the plugin-scoped MCP launcher overrides that setup touched. It never copies provider definitions, auth stores, account identifiers, or credentials. A normal clean setup contains generated policy text, the namespace value, seat IDs, and restoration metadata. Explicit replacement must retain the user's exact old hint text so disable can restore it; routing hints must never contain credentials. State is written with a same-directory atomic replacement and restrictive file mode where supported. If persistence fails after config apply, the configurator rolls the config back using the returned version.
Disable compares every current managed value before restoration. If the user edited a managed field after setup, it stops instead of erasing that work. Without state, each surviving marker proves ownership only of that hint string. Disable may safely remove the marked string or strings, but it leaves metadata visibility and the tool namespace unchanged because their previous values are unknown.
Shared-config compatibility
Desktop and CLI commonly share ~/.codex/config.toml. A field supported by Desktop can prevent an older CLI from starting at all.
Setup automatically checks the installations it can identify:
- the supplied active-host binary;
codexon PATH when different;- the macOS Desktop embedded binary when present;
- every explicit
--compat-bin.
Ask the user about alternate Desktop, IDE, container, or Windows installations that share the same home, because no open-source installer can discover every possible binary path. Pass each known path with --compat-bin.
If any checked binary rejects the complete preset, normal setup fails before writing. Preferred resolution: update that client. The per-task skill workflow remains available without a global policy. Successful parsing does not prove that a future task selected a v2 root or that a live model route was accepted.
--allow-incompatible-client is an escape hatch only after the user explicitly accepts that the named client may stop loading the shared config. Disable never blocks on this compatibility check; otherwise the policy could trap the user.
Custom agents
Codex's reusable role format is one TOML file per custom agent:
<project>/.codex/agents/*.toml
~/.codex/agents/*.tomlProject-scoped/legacy saves use these fixed names:
codex-orchestration-executor.toml -> codex_orchestration_executor
codex-orchestration-advisor.toml -> codex_orchestration_advisorPersonal roles used by the global native policy add a stable 12-character suffix derived from the canonical CODEX_HOME path:
codex-orchestration-executor-<personal-id>.toml -> codex_orchestration_executor_<personal-id>
codex-orchestration-advisor-<personal-id>.toml -> codex_orchestration_advisor_<personal-id>This prevents accidental shadowing by the older fixed project names. The native configurator requires exactly one matching personal role and refuses a same-name project role in the current workspace. Because project roles have higher precedence, run status in each project before relying on a personal custom-agent route; a deliberately duplicated suffixed name can still shadow it.
The executor file says to implement only the root's bounded packet, preserve unrelated work, verify, report, and never spawn. The advisor file says to review only the root's packet, request a read-only sandbox, return PLAN_APPROVED or PLAN_REVISE, and never edit, delegate, or contact executors.
Custom agents load in a new task. Writing a file does not hot-load it into an existing task. A project-scoped role loads only from a trusted project. If the same role name exists in project and personal scope, report the collision instead of guessing precedence.
Treat a saved scope as one complete team anchored by its executor. A missing same-scope advisor means advisor: none; never silently borrow an advisor from another scope.
The standalone-agent configurator remains dry-run first, rejects symlinks and hard links, preserves supported metadata, journals multi-file transactions without storing config contents, refuses edited or user-owned files, and uses opt-in backup-first migration for known output from versions 0.1–0.3.
The role-file transaction and native App Server policy transaction are independent. After a phase-two failure, remove only fully validated newly managed roles. Native status reports collision-resistant managed personal roles that are not referenced by its current restore state, and --require-effective treats them as unhealthy. This recovery is compensating cleanup, not atomicity across the two stores.
On Windows, the custom-agent configurator can create a new role but refuses in-place update or removal of an existing managed role because it cannot prove the Unix inode/metadata-preservation contract. Native App Server policy setup and disable are separate and remain capability-tested through the active Codex binary.
Provider boundaries
Direct v2 model overrides retain the parent's provider. They are the simplest route for an OpenAI root and OpenAI Luna/Terra child.
Claude Fable 5 is the explicit built-in exception. The plugin does not pretend it is a Codex model or translate Anthropic into the Responses protocol. Instead, a disabled-by-default local MCP server invokes the official claude CLI with the user's first-party Pro or Max login. Setup enables one Python 3.11+ launcher variant, and disable restores every prior plugin override value. Codex's TOML editor can retain an inert empty table header after its final key is deleted; the configurator does not risk a broad TOML rewrite for cosmetic cleanup.
The bridge removes ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, and Bedrock/Vertex/Foundry selection variables from the child environment. It re-checks claude auth status, pins claude-fable-5 and the saved effort, disables tools and session persistence, disables prompt suggestions, and requires JSON runtime metadata to confirm the model. Setup and status never make a model call.
A cross-provider seat normally needs:
1. a provider already defined and authenticated in the user's Codex config; 2. a personal custom agent that pins the provider, model, and effort; 3. a new task that loads that agent; 4. v2 spawn with the matching agent_type and fork_turns = "none".
Never create provider definitions, request keys in chat, write credentials, or imply that an OpenAI login grants access to another provider.
Codex custom providers currently use the Responses wire protocol. An Anthropic Messages endpoint is not automatically compatible. Use a supported integration that the user has configured and tested, such as an appropriate Amazon Bedrock route where available.
Advisor permissions
A task-local advisor is review-only by instruction. Do not claim it is mechanically read-only unless the effective child sandbox confirms that.
A saved advisor requests sandbox_mode = "read-only", but live parent permission overrides may be reapplied to children. Keep the behavioral prohibition on edits and mutation even with the requested sandbox.
The Claude Fable 5 advisor is mechanically narrower than a child: the MCP tool accepts only a review packet, launches Claude with safe mode and no tools, and exposes no edit or shell operation. It still has open-world model access, so the packet must be deliberate and self-contained.
Advisor failure is never approval. A configured advisor is required for a non-trivial executor plan unless the user explicitly marks it best-effort. Transport failure, malformed output, missing context, or wrong route becomes advisor unavailable; stop before executor work by default, or disclose and continue under the root only in best-effort mode.
Goals and task lifetime
This skill does not create, start, pause, clear, or alter a Goal. If the user already runs a Goal, the routing policy works inside the same Codex delegation flow.
Even when the write API requests user-config reload, this transient installer cannot retroactively rewrite the developer policy already compiled into another task. Start a new task after setup, update, disable, or custom-agent changes.
A personal policy can be overridden by a trusted project's .codex/config.toml or a managed layer. Run status from the target workspace. “Policy installed” describes the user layer; “effective in this workspace” additionally confirms that no higher-precedence layer replaces the managed fields there. Neither status proves that the model selected for a future task activates v2.
Named profile-v2 files are separate selected user layers. The default command does not start App Server with --profile, so its write/readback does not verify a named profile. A profile user must inspect that layer separately and ensure it does not override the four routing fields, or use the task-local fallback.
Concurrency and service tier
The effective concurrency limit belongs to the active Codex version and agents.max_threads configuration. This plugin never changes that limit or forces a worker count. Codex should parallelize only independent slices with non-overlapping write ownership.
Child service tier can inherit from the parent when supported. There is no portable “force standard tier” spawn setting that works across current catalogs. If allowance savings are the priority, do not enable Fast/priority on the root.
Truthful route states
Use precise language:
native policy installed: managed user policy exists; activation still depends on root model and effective workspace config;policy effective: the managed fields win in the current workspace; this is still not a live spawn;pinned custom agent available: matching role loaded, not yet used;route accepted: exact controls were accepted and validated by the current tool;unverified prompt preference: no exact control available;used and confirmed: only when the client explicitly exposes effective runtime model/provider/effort metadata;inherited root — requested child model was not used;unavailable: provider/model/selector cannot run;none: advisor disabled.
Requested text, a config file, or child prose alone is not proof that a model ran.
Usage and savings language
Keep these concepts separate:
- Raw tokens: every input, cached input, output, context, and tool-result token. Subagents can increase this total.
- Codex credits: token usage weighted by model-specific rates.
- Included limits: shared five-hour usage plus any applicable weekly limits; real consumption depends on model, context, reasoning, tools, caching, tier, and plan.
- Other-provider usage: separate billing or allowance.
The defensible “about 65%” example is:
20% Sol + 80% Luna at 20% of Sol's token credit rate
= 0.20 + (0.80 × 0.20)
= 0.36, or about 64% fewer credits before orchestration overheadNever promise 65% fewer raw tokens, a fixed weekly saving, a universal monetary saving, or five times more completed work.
Primary sources
#!/usr/bin/env python3
"""Read-only MCP bridge from Codex to Claude Fable 5 through Claude Code."""
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
from typing import Any
STATE_FILENAME = ".codex-orchestration-routing.json"
FABLE_MODEL = "claude-fable-5"
SUPPORTED_EFFORTS = {"low", "medium", "high", "max"}
CLAUDE_TIMEOUT_SECONDS = 600
AUTH_TIMEOUT_SECONDS = 20
SENSITIVE_ENV = {
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_VERTEX",
"CLAUDE_CODE_USE_FOUNDRY",
}
SYSTEM_PROMPT = """You are Claude Fable 5 acting only as a plan advisor to Codex's root orchestrator.
Review the supplied self-contained packet for material correctness, missing constraints, unsafe sequencing, ownership conflicts, and verification gaps. Do not edit files, call tools, spawn agents, contact executors, or attempt implementation.
Your first non-empty line must be exactly PLAN_APPROVED or PLAN_REVISE.
Use PLAN_APPROVED only when no material gap is present. Use PLAN_REVISE when correction is needed, followed by a concise prioritized list in which every gap has a concrete correction. Ignore style preferences. Report only to the root orchestrator."""
class AdvisorError(RuntimeError):
pass
def codex_home() -> Path:
value = os.environ.get("CODEX_HOME")
return Path(value).expanduser() if value else Path.home() / ".codex"
def sanitized_environment() -> dict[str, str]:
env = os.environ.copy()
for name in SENSITIVE_ENV:
env.pop(name, None)
return env
def resolve_claude() -> Path:
found = shutil.which("claude")
if found:
return Path(found).resolve()
candidates = (
Path.home() / ".local" / "bin" / "claude",
Path("/usr/local/bin/claude"),
Path("/opt/homebrew/bin/claude"),
)
for candidate in candidates:
if candidate.is_file():
return candidate.resolve()
raise AdvisorError("Claude Code is not installed or `claude` is not on PATH.")
def _run_json(command: list[str], *, timeout: int) -> dict[str, Any]:
try:
result = subprocess.run(
command,
env=sanitized_environment(),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise AdvisorError(f"Could not run Claude Code: {exc}") from exc
if result.returncode != 0:
raise AdvisorError(f"Claude Code exited with {result.returncode}.")
try:
payload = json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise AdvisorError("Claude Code returned malformed JSON.") from exc
if not isinstance(payload, dict):
raise AdvisorError("Claude Code returned an unexpected JSON value.")
return payload
def check_claude_auth(claude: Path | None = None) -> dict[str, str]:
executable = claude or resolve_claude()
payload = _run_json([str(executable), "auth", "status"], timeout=AUTH_TIMEOUT_SECONDS)
subscription = payload.get("subscriptionType")
if not (
payload.get("loggedIn") is True
and payload.get("authMethod") == "claude.ai"
and payload.get("apiProvider") == "firstParty"
and subscription in {"pro", "max"}
):
raise AdvisorError(
"Claude Code must be logged in through a first-party Pro or Max account; "
"run `claude auth login` and try again."
)
return {
"auth_method": "claude.ai",
"api_provider": "firstParty",
}
def load_fable_route(home: Path | None = None) -> dict[str, str]:
path = (home or codex_home()) / STATE_FILENAME
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise AdvisorError("Claude Fable 5 is not configured; run setup first.") from exc
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise AdvisorError(f"Could not read the routing state: {exc}") from exc
route = payload.get("advisor") if isinstance(payload, dict) else None
if not isinstance(route, dict) or route.get("kind") != "fable":
raise AdvisorError("Claude Fable 5 is not the configured advisor.")
model = route.get("model")
effort = route.get("effort")
if model != FABLE_MODEL or effort not in SUPPORTED_EFFORTS:
raise AdvisorError("The saved Claude Fable 5 route is invalid.")
return {"model": model, "effort": effort}
def review_plan(packet: str) -> dict[str, Any]:
if not isinstance(packet, str) or not packet.strip():
raise AdvisorError("`packet` must be a non-empty self-contained review packet.")
route = load_fable_route()
claude = resolve_claude()
auth = check_claude_auth(claude)
command = [
str(claude),
"-p",
"--model",
route["model"],
"--effort",
route["effort"],
"--safe-mode",
"--tools",
"",
"--permission-mode",
"dontAsk",
"--no-session-persistence",
"--prompt-suggestions",
"false",
"--output-format",
"json",
"--system-prompt",
SYSTEM_PROMPT,
]
try:
result = subprocess.run(
command,
input=packet,
env=sanitized_environment(),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=CLAUDE_TIMEOUT_SECONDS,
check=False,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise AdvisorError(f"Claude Fable 5 review failed: {exc}") from exc
if result.returncode != 0:
detail = result.stderr.strip() or result.stdout.strip() or "no diagnostic"
raise AdvisorError(f"Claude Fable 5 exited with {result.returncode}: {detail}")
try:
payload = json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise AdvisorError("Claude Fable 5 returned malformed JSON.") from exc
if not isinstance(payload, dict) or not isinstance(payload.get("result"), str):
raise AdvisorError("Claude Fable 5 returned an unexpected response.")
review = payload["result"].strip()
first = next((line.strip() for line in review.splitlines() if line.strip()), "")
if first not in {"PLAN_APPROVED", "PLAN_REVISE"}:
raise AdvisorError("Claude Fable 5 omitted the required plan decision.")
usage = payload.get("modelUsage")
used_models = sorted(usage) if isinstance(usage, dict) else []
if FABLE_MODEL not in used_models:
raise AdvisorError("Runtime metadata did not confirm Claude Fable 5.")
return {
"decision": first,
"review": review,
"model": FABLE_MODEL,
"effort": route["effort"],
"auth_method": auth["auth_method"],
"used_models": used_models,
}
def tool_definitions() -> list[dict[str, Any]]:
annotations = {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": True,
}
return [
{
"name": "review_plan",
"title": "Review a plan with Claude Fable 5",
"description": (
"Send one self-contained, read-only plan-review packet to the configured "
"Claude Fable 5 advisor."
),
"inputSchema": {
"type": "object",
"properties": {
"packet": {
"type": "string",
"description": "Complete context, plan, risks, slices, and checks.",
}
},
"required": ["packet"],
"additionalProperties": False,
},
"annotations": annotations,
},
{
"name": "status",
"title": "Check Claude Fable 5 advisor status",
"description": "Check the saved route and Claude Code login without a model call.",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": False,
},
"annotations": annotations,
},
]
def _tool_result(payload: dict[str, Any], *, is_error: bool = False) -> dict[str, Any]:
return {
"content": [{"type": "text", "text": json.dumps(payload, sort_keys=True)}],
"isError": is_error,
}
def handle_request(request: dict[str, Any]) -> dict[str, Any] | None:
method = request.get("method")
request_id = request.get("id")
if request_id is None:
return None
if method == "initialize":
result = {
"protocolVersion": "2025-06-18",
"capabilities": {"tools": {"listChanged": False}},
"serverInfo": {"name": "codex-orchestration-fable-advisor", "version": "1.0.0"},
}
elif method == "ping":
result = {}
elif method == "tools/list":
result = {"tools": tool_definitions()}
elif method == "tools/call":
params = request.get("params")
name = params.get("name") if isinstance(params, dict) else None
arguments = params.get("arguments", {}) if isinstance(params, dict) else {}
try:
if name == "review_plan":
packet = arguments.get("packet") if isinstance(arguments, dict) else None
result = _tool_result(review_plan(packet))
elif name == "status":
route = load_fable_route()
auth = check_claude_auth()
result = _tool_result({"available": True, **route, **auth})
else:
raise AdvisorError(f"Unknown tool: {name!r}.")
except AdvisorError as exc:
result = _tool_result({"available": False, "error": str(exc)}, is_error=True)
else:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32601, "message": f"Method not found: {method}"},
}
return {"jsonrpc": "2.0", "id": request_id, "result": result}
def main() -> int:
for line in sys.stdin:
try:
request = json.loads(line)
if not isinstance(request, dict):
raise ValueError("request must be an object")
response = handle_request(request)
except (json.JSONDecodeError, ValueError) as exc:
response = {
"jsonrpc": "2.0",
"id": None,
"error": {"code": -32700, "message": str(exc)},
}
if response is not None:
print(json.dumps(response, separators=(",", ":")), flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Print a compact view of the model catalog exposed by Codex."""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
PROVIDER_RE = re.compile(r"^[A-Za-z0-9_-]+$")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Inspect the model catalog exposed by the installed Codex CLI."
)
parser.add_argument("--provider", help="Optional configured Codex provider ID.")
parser.add_argument("--bundled", action="store_true", help="Skip catalog refresh.")
parser.add_argument("--json", action="store_true", help="Emit compact JSON.")
parser.add_argument("--codex-bin", default="codex", help="Codex executable name or path.")
return parser.parse_args()
def resolve_executable(codex_bin: str) -> str:
executable = shutil.which(codex_bin) if "/" not in codex_bin else codex_bin
if not executable:
raise RuntimeError(f"Codex executable not found: {codex_bin}")
path = Path(executable).expanduser()
if not path.is_file() or not os.access(path, os.X_OK):
raise RuntimeError(f"Codex executable is not a regular executable file: {path}")
return str(path.resolve())
def inspect_version(executable: str) -> str:
try:
completed = subprocess.run(
[executable, "--version"],
capture_output=True,
text=True,
check=False,
timeout=15,
)
except (OSError, subprocess.TimeoutExpired, UnicodeDecodeError):
return "unknown"
value = completed.stdout.strip() or completed.stderr.strip()
return value if completed.returncode == 0 and value else "unknown"
def load_catalog(
codex_bin: str, provider: str | None, bundled: bool
) -> tuple[dict[str, Any], str, str]:
executable = resolve_executable(codex_bin)
command = [executable, "debug", "models"]
if bundled:
command.append("--bundled")
if provider:
if not PROVIDER_RE.fullmatch(provider):
raise RuntimeError(f"Invalid provider ID: {provider!r}")
command.extend(["-c", f'model_provider="{provider}"'])
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
check=False,
timeout=30,
)
except subprocess.TimeoutExpired as exc:
raise RuntimeError("Codex model inspection timed out after 30 seconds") from exc
except UnicodeDecodeError as exc:
raise RuntimeError("Codex model inspection returned invalid UTF-8") from exc
except OSError as exc:
raise RuntimeError(f"Could not run Codex model inspection: {exc}") from exc
if completed.returncode != 0:
detail = completed.stderr.strip() or completed.stdout.strip() or "unknown error"
raise RuntimeError(f"Codex model inspection failed: {detail}")
try:
payload = json.loads(completed.stdout)
except json.JSONDecodeError as exc:
raise RuntimeError(f"Codex returned invalid model JSON: {exc}") from exc
if not isinstance(payload, dict):
raise RuntimeError("Codex model response root is not an object")
if not isinstance(payload.get("models"), list):
raise RuntimeError("Codex model response does not contain a models array")
for index, model in enumerate(payload["models"]):
if not isinstance(model, dict) or not isinstance(model.get("slug"), str):
raise RuntimeError(
f"Codex model response has an invalid model entry at index {index}"
)
levels = model.get("supported_reasoning_levels")
if levels is not None and not isinstance(levels, list):
raise RuntimeError(
f"Codex model response has invalid reasoning levels at index {index}"
)
for level_index, level in enumerate(levels or []):
if (
not isinstance(level, dict)
or not isinstance(level.get("effort"), str)
):
raise RuntimeError(
"Codex model response has an invalid reasoning level at "
f"model index {index}, level index {level_index}"
)
return payload, executable, inspect_version(executable)
def compact_model(model: dict[str, Any]) -> dict[str, Any]:
raw_levels = model.get("supported_reasoning_levels")
levels = raw_levels if isinstance(raw_levels, list) else []
efforts = [
item["effort"]
for item in levels
if isinstance(item, dict) and isinstance(item.get("effort"), str)
]
return {
"id": model.get("slug"),
"display_name": model.get("display_name"),
"description": model.get("description"),
"default_effort": model.get("default_reasoning_level"),
"supported_efforts": [effort for effort in efforts if effort],
"visibility": model.get("visibility"),
}
def print_table(models: list[dict[str, Any]]) -> None:
rows = []
for model in models:
rows.append(
(
str(model.get("id") or ""),
",".join(model.get("supported_efforts") or []) or "default only",
str(model.get("description") or ""),
)
)
id_width = max([len("MODEL ID"), *(len(row[0]) for row in rows)])
effort_width = max([len("EFFORTS"), *(len(row[1]) for row in rows)])
print(f"{'MODEL ID':<{id_width}} {'EFFORTS':<{effort_width}} DESCRIPTION")
for model_id, efforts, description in rows:
print(f"{model_id:<{id_width}} {efforts:<{effort_width}} {description}")
def main() -> int:
args = parse_args()
try:
payload, executable, version = load_catalog(
args.codex_bin, args.provider, args.bundled
)
models = [compact_model(model) for model in payload["models"]]
except RuntimeError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
catalog_source = (
"codex debug models --bundled" if args.bundled else "codex debug models"
)
if args.json:
print(
json.dumps(
{
"codex_binary": executable,
"codex_version": version,
"catalog_source": catalog_source,
"provider": args.provider,
"models": models,
},
indent=2,
)
)
else:
print(
f"Catalog: {version} at {executable} ({catalog_source})",
file=sys.stderr,
)
print_table(models)
print(
"\nNote: the active desktop or remote host may expose newer models than this CLI catalog.",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())