Writing
Sep 19, 2026/11 min read

One Router, Two Coding Agents, Lower Costs

Coding agents become expensive when every delegated task uses the most expensive model available.

If Codex runs on GPT-6 Astra (gpt-6-astra) and sends every subtask to GPT-6 Astra too, premium reasoning pays for file searches, routine edits, test runs, and small repairs. Claude Code has the same problem when Claude Fable 5.1 (claude-fable-5-1) delegates every task to another Fable 5.1 session.

Most of those tasks already have a destination. A primary agent can inspect the repository, make the architectural decision, and write a precise brief. A cheaper, faster model can then implement that brief, run the tests, fix a failure, and respond to review. The extra turns stay affordable because the executor is not spending premium reasoning tokens on decisions it does not need to make.

I fixed this for myself by putting one Rust router in front of both Codex and Claude Code. GPT-6 Astra or Claude Fable 5.1 remains the lead: it reads the repository, reasons through the architecture, and decides what done looks like. A lower-cost model—GPT-5.6 Luna (gpt-5.6-luna), DeepSeek V4.1 Flash (deepseek-flash), or GLM 5.3 Flash (glm-5.3-flash)—works underneath it as the executor, handling bounded edits, tests, and repair turns.

That hierarchy matters. The expensive model is still in control; I’ve stopped paying it to do every mechanical step. The router makes the handoff practical by giving both coding agents access to the same pool of lower-cost workers.

One router, two protocol edges

Codex and Claude Code do not send the same API shape. Codex uses the OpenAI Responses API. Claude Code uses the Anthropic Messages API. The router gives each client the edge it expects, then selects a provider from the model name.

Codex ── Responses ─▶ /backend-api/codex ──┬─▶ OpenAI Responses
                                           ├─▶ DeepSeek Responses
                                           ├─▶ OpenRouter Responses
                                           └─▶ Z.ai Chat Completions

Claude Code ── Messages ─▶ /backend-api/claude/v1/messages
                                           ├─▶ Anthropic Messages
                                           ├─▶ DeepSeek Messages
                                           ├─▶ OpenRouter Messages
                                           └─▶ Z.ai Messages

The implementation used in this guide is codex-router. It keeps provider credentials on the router, chooses a route from the model prefix, and translates only where a provider needs another request format.

Prerequisites

You need:

  • Rust and Cargo
  • Codex, Claude Code, or both
  • An API key for each provider you intend to use
  • A model catalog for the Codex picker if you want custom Codex entries

Keep the router on 127.0.0.1 while you are setting it up. Store provider keys outside the repository. The router reads these conventional paths:

~/.config/deepseek/key
~/.config/zai/key
~/.config/openrouter/key
~/.config/meta/key

Make those files readable only by your user. Do not put keys in config.toml, a model catalog, a launch agent, or a shell command that will remain in history.

For example, create the DeepSeek key file with an editor, then lock down its permissions:

install -d -m 700 "$HOME/.config/deepseek"
${EDITOR:-vi} "$HOME/.config/deepseek/key"
chmod 600 "$HOME/.config/deepseek/key"

Repeat that pattern only for the providers you plan to use.

Terminal 1: build and run the router

git clone https://github.com/iamngoni/codex-router.git
cd codex-router
cargo build --release
./target/release/codex-router

Leave that process running. The default local listener is 127.0.0.1:4141.

Terminal 2: check the local service

curl http://127.0.0.1:4141/healthz

The response should be ok. If it is not, fix the router process before configuring either coding agent.

Connect Codex

Add a provider entry to ~/.codex/config.toml:

model_provider = "router"
model_catalog_json = "/absolute/path/to/model-catalog.json"

[model_providers.router]
name = "Local model router"
base_url = "http://127.0.0.1:4141/backend-api/codex"
wire_api = "responses"
requires_openai_auth = true

The router’s Codex edge speaks Responses. requires_openai_auth = true keeps the existing Codex authentication path available for models that remain on the ChatGPT-backed route. This is a Codex provider configuration, not a general OpenAI API-key proxy.

Add custom models to the catalog referenced above. Merge the entry into your existing catalog instead of replacing the whole file. Use a complete entry: Codex reads the capability fields as well as the name and context window.

{
  "models": [
    {
      "slug": "deepseek-flash",
      "display_name": "DeepSeek V4.1 Flash",
      "description": "DeepSeek V4.1 Flash through your DeepSeek API account.",
      "default_reasoning_level": "low",
      "supported_reasoning_levels": [
        {
          "effort": "low",
          "description": "Low reasoning"
        },
        {
          "effort": "high",
          "description": "High reasoning"
        },
        {
          "effort": "max",
          "description": "Max reasoning"
        }
      ],
      "shell_type": "shell_command",
      "visibility": "list",
      "supported_in_api": true,
      "priority": 1,
      "base_instructions": "You are a coding assistant. Complete the specified task accurately, use the available tools, and verify your changes.",
      "context_window": 1048576,
      "max_context_window": 1048576,
      "effective_context_window_percent": 95,
      "truncation_policy": {
        "mode": "tokens",
        "limit": 10000
      },
      "input_modalities": [
        "text",
        "image"
      ],
      "apply_patch_tool_type": "freeform",
      "support_verbosity": true,
      "default_verbosity": "low",
      "default_reasoning_summary": "none",
      "supports_parallel_tool_calls": true,
      "use_responses_lite": false,
      "prefer_websockets": false,
      "experimental_supported_tools": []
    }
  ]
}

This entry uses low reasoning by default because its job is execution from a clear brief. You can still select high or max for a task that needs more judgment. Catalog fields can change between Codex releases, so if your installed catalog uses a newer shape, copy a complete built-in entry from that release and preserve its capability fields.

The catalog controls what Codex shows. The router still has to accept the selected model, the provider has to allow it, and the account has to be able to pay for it. A picker entry alone does not prove that a route works.

The router uses the model prefix to choose the upstream. deepseek-* goes to DeepSeek, openrouter/* goes to OpenRouter after the local prefix is removed, and glm-* goes to Z.ai. Codex passes DeepSeek and OpenRouter Responses traffic through with tool-schema cleanup. The GLM path translates Responses to Chat Completions and translates the response back. Codex pass-through responses currently buffer before returning, so this edge is compatible with the Responses contract but does not provide token-by-token streaming for those routes.

Select deepseek-flash from the Codex model picker and run a short task. Once that works, add the other provider entries one at a time.

Connect Claude Code

Claude Code needs the router’s Messages edge. Set the base URL like this:

export ANTHROPIC_BASE_URL="http://127.0.0.1:4141/backend-api/claude"
claude --model deepseek-flash

The value is a base URL. Claude Code appends the standard Anthropic path, so the request reaches:

POST /backend-api/claude/v1/messages

That /v1/messages suffix identifies the Messages operation. Do not add it to ANTHROPIC_BASE_URL, or Claude Code will append it twice.

You can add custom choices to Claude Code’s model picker in ~/.claude/settings.json:

{
  "modelPicker": {
    "replaceBuiltInOptions": false,
    "options": [
      {
        "model": "deepseek-flash",
        "label": "DeepSeek V4.1 Flash",
        "description": "DeepSeek through the local router",
        "behavesAs": "claude-sonnet-5"
      },
      {
        "model": "glm-5.3-flash",
        "label": "GLM 5.3 Flash",
        "description": "Z.ai through the local router",
        "behavesAs": "claude-sonnet-4-5"
      },
      {
        "model": "openrouter/openai/gpt-5-nano",
        "label": "GPT-5 Nano through OpenRouter",
        "description": "OpenRouter through the local router",
        "behavesAs": "claude-sonnet-4-5"
      }
    ]
  }
}

The exact picker options vary between Claude Code releases, so keep the model strings aligned with the router’s route prefixes. The gateway receives model and uses it to choose the provider. behavesAs gives the custom model a capability profile Claude Code already understands; it does not change the model ID sent to the router. That profile also affects context, compaction, tool, and effort assumptions, so choose one whose limits do not exceed the real upstream model. replaceBuiltInOptions: false keeps the standard Claude choices beside the custom routes.

The Claude edge sends native Messages traffic to DeepSeek, GLM, and OpenRouter. It does not flatten a tool call into plain text or translate the request through the Codex Responses format. That preserves the content blocks and server-sent events Claude Code expects.

Send execution work to cheaper models

The model split belongs in the instructions that guide your agents. In a Codex AGENTS.md, use a policy like this:

Use GPT-6 Astra for planning, architecture, difficult diagnosis, and final review.

Delegate bounded implementation to GPT-5.6 Luna by default. DeepSeek V4.1
Flash and GLM 5.3 Flash are lower-cost alternatives for clearly specified work.
Let those executors implement, test, and repair the change across several turns.
Return architectural ambiguity and the final acceptance decision to Astra.

The same idea works in Claude Code with its subagent model setting:

export ANTHROPIC_BASE_URL="http://127.0.0.1:4141/backend-api/claude"
export CLAUDE_CODE_SUBAGENT_MODEL="deepseek-flash"

claude --model fable

The primary Claude model can stay on the fable alias for Claude Fable 5.1 (claude-fable-5-1) while the environment variable makes DeepSeek V4.1 Flash the default for subagents that do not choose another model. An explicit model in a tool call or agent definition takes precedence. Replace deepseek-flash with glm-5.3-flash when that route fits the task better.

Lower reasoning is a cost decision, not a claim that every task is interchangeable. A model with less reasoning is useful when the destination is explicit: edit these files, implement this function, run these tests, or repair this reported failure. Give it an ambiguous architecture problem and you are asking it to invent the specification as well as execute it. Keep that work with the primary model.

The savings show up across turns. An executor may need one turn to inspect files, another to implement, another to run tests, and another to repair a failure. Fast lower-cost models make that loop affordable. The point is to stop paying for premium reasoning after the hard decision has already been made.

Give the executor a brief with a destination, constraints, and a definition of done. For example:

Implement the approved session-timeout change in `src/auth/session.rs`.

- Keep the public API unchanged.
- Do not edit unrelated authentication code.
- Add tests for an expired session and a valid session.
- Run the focused auth tests and report the command and result.
- If the approved design cannot work, stop and return the exact conflict.

That is the kind of work GPT-5.6 Luna, DeepSeek V4.1 Flash, or GLM 5.3 Flash can carry through several implementation and repair turns without making every turn a premium-model decision.

Run a plain check and a tool check

With the router running, verify the Claude edge from a third terminal or shell session:

export ANTHROPIC_BASE_URL="http://127.0.0.1:4141/backend-api/claude"

claude --model deepseek-flash -p 'Reply with exactly: router-ok'

Then ask Claude Code to use a tool:

claude --model deepseek-flash -p 'Read README.md and tell me its first heading.'

The second command checks more than text generation. It exercises Claude Code’s Messages request, the streamed tool_use block, the returned tool_result, and the follow-up API turn.

In the live router, DeepSeek completed both checks. The streamed response included message_start, keepalive pings, thinking and text deltas, message_delta, and message_stop. The Read tool call completed as a real tool_usetool_result → final-answer round trip.

GLM 5.3 Flash and GPT-5 Nano through OpenRouter returned plain Claude Code responses through the same Messages edge. That confirms those routes and their streaming responses; it does not claim that their tool-use paths have been tested here.

Native Claude follows the same edge. A claude-fable-5-1 request reached Anthropic with the caller’s credential. The account returned credits_required, so successful Fable generation was blocked by account credits rather than by the router.

What the router changes on each path

The route is selected from the model name, but the protocol and credential rules stay separate:

Client and modelUpstream behaviorAuthentication and streaming
Codex + deepseek-* or openrouter/*Responses pass-through with request cleanupProvider key replaces the incoming credential; responses buffer before return
Codex + glm-*Responses to Chat Completions and backProvider key replaces the incoming credential; translated response is buffered
Claude Code + deepseek-*, glm-*, or openrouter/*Native Anthropic Messages request and responseProvider key replaces Claude auth; Messages SSE streams incrementally
Claude Code + claude-*Native Anthropic Messages pass-throughCaller’s Anthropic credential is preserved

On the Claude Messages edge, the router strips incoming cookies, x-api-key, and caller authorization before a third-party route receives the request. It inserts the key belonging to the selected provider. Native Claude routes are the exception because preserving the caller’s Anthropic credential is what lets normal Claude authentication work.

The openrouter/ prefix is local naming. openrouter/openai/gpt-5-nano becomes openai/gpt-5-nano at OpenRouter. The muse-* namespace has no Claude Messages route and returns an explicit unsupported-model error.

Troubleshooting

Claude Code returns a 404. Set ANTHROPIC_BASE_URL to /backend-api/claude, without /v1/messages. Claude Code appends that path. An unknown path under the Claude namespace is rejected locally rather than falling through to the Codex handler.

A provider returns an authentication error. Check the selected model prefix and the matching key file. Third-party Claude routes use the router’s provider key, while claude-* routes use the caller’s Anthropic credential.

The model appears in the picker but fails on its first request. Check provider allowlists, account credits, billing, context limits, and the route’s actual API support. A catalog entry is metadata, not a connectivity test.

A tool call works on DeepSeek but not on another provider. Treat tool support as route-specific. The live proof above covers a DeepSeek Claude tool round trip, not GLM or OpenRouter tool use. Start with a plain response, then test the exact tool flow you need.

A long Claude conversation fails while Codex handles a similar history. The Codex DeepSeek trim pipeline does not apply to the Claude Messages edge. Claude Messages requests pass beneath the router’s general 100 MiB body ceiling and the selected provider decides whether the history fits. Images, tool output, and thinking blocks all count toward the real request size.

Logs contain more than expected. Request previews and complete provider error bodies can contain repository context, tool output, or credentials accidentally included by an upstream. Treat the logs as sensitive and reduce them to redacted summaries before exposing the router beyond your machine.

The practical next step

Start with one route and one client. Put Codex or Claude Code behind the local edge, run the plain check, then run the tool check. Add a second provider only after you have confirmed its request shape, authentication, streaming behavior, and tool contract.

Once the route works, move the cost boundary into your agent instructions: premium reasoning for planning and acceptance, lower-cost execution for a brief with a clear destination. That gives you more room for the turns real software work needs without making every turn pay the highest model price.