Thursday, October 22, 2026

Claude Code Just Got Diversity

Claude Code Just Got Diversity

Claude Code sessions have been able to message each other since August. What I have not seen anyone doing yet is putting a different vendor's model behind each session and keeping the messaging working: Claude on my subscription, open weights through Ollama, ten vendors through OpenRouter, a model on my own GPU, seventeen seats in one roster, each with a role, getting work done and validated among themselves.


What is new here, and what is not

Cross-session messaging shipped in Claude Code v2.1.224, the week of August 3 to 7, 2026. Claude discovers your other sessions on the machine with a ListAgents tool and sends with SendMessage. The message is text one Claude writes for another, delivered over a per-session Unix socket that never touches Anthropic's servers. Two months on, that is table stakes, and the docs cover it well.

What the docs assume, quietly, is that every session on the far end is Claude.

It does not have to be. In July I wrote about running Claude Code against Ollama. Claude Code is an agent harness that happens to ship pointed at Anthropic. Change the base URL and it is the same harness, same tool loop, same skills, with a different brain behind it. Any provider that speaks the Anthropic Messages API can sit behind a session, and Ollama and OpenRouter both do, natively.

Put the two together and you get something no vendor has an incentive to build: a team of Claude Code sessions where each seat runs a different lab's weights, and the harness's own messaging lets them coordinate without you in the middle. The messaging is the harness's job, not the model's.

The reason I care is not the plumbing. It is the point of view each seat brings. My last post, on October 4, was about the Developer, Reviewer, Critic team I have run across providers for over a year, with a Security seat and a PMO alongside. The value was never that one model is smarter. It is that a Reviewer on one lab's weights does not share the Developer's blind spots, and a Critic on a third lab's weights refuses to inherit either of their trust. Until now the handoffs between those seats went through me or through a database. Now the Developer messages the Reviewer, the Reviewer re-runs it and messages the Critic, and the Critic rules, each in its own session with its own brain.


The roster

This is ListAgents as seen by a coordinator session I named DESK, running Claude Sonnet on my Max subscription, in a fresh config directory set up for the demo:

This session is DESK [9cf94c] — the name other sessions use to message it.

Peer sessions (17):
  OLLAMA_KIMI [a2b9de]      ·  bg           ·  idle  ·  started 4m ago
  OLLAMA_GLM [a55b4d]       ·  bg           ·  idle  ·  started 4m ago
  HAIKU [7548a8]            ·  bg           ·  idle  ·  started 4m ago
  OLLAMA_MISTRAL [065dae]   ·  bg           ·  idle  ·  started 4m ago
  MISTRAL [1b5bcc]          ·  bg           ·  idle  ·  started 5m ago
  3k1o-dc [c5700a]          ·  interactive  ·  idle  ·  started 34m ago
  GEMINI [36dad3]           ·  bg           ·  idle  ·  started 5m ago
  GPT [c48808]              ·  bg           ·  idle  ·  started 5m ago
  OLLAMA_DEEPSEEK [ea64fa]  ·  bg           ·  idle  ·  started 4m ago
  OPUS [c14fb0]             ·  bg           ·  idle  ·  started 6m ago
  LOCAL_GPTOSS [791bc3]     ·  bg           ·  idle  ·  started 4m ago
  DEEPSEEK [233cde]         ·  bg           ·  idle  ·  started 5m ago
  KIMI [8b7d6c]             ·  bg           ·  idle  ·  started 5m ago
  LLAMA [fda21a]            ·  bg           ·  idle  ·  started 5m ago
  GROK [c01747]             ·  bg           ·  idle  ·  started 5m ago
  GLM [ee1cae]              ·  bg           ·  idle  ·  started 4m ago
  QWEN [100b93]             ·  bg           ·  idle  ·  started 5m ago
Source Seats Model tags, from each seat's own transcript Meter
Anthropic, direct DESK, OPUS claude-sonnet-5-5, claude-opus-5-5 Claude Max subscription
Ollama Cloud OLLAMA_KIMI, OLLAMA_MISTRAL, OLLAMA_GLM, OLLAMA_DEEPSEEK kimi-k3:cloud, mistral-large-4:cloud, glm-5.3:cloud, deepseek-v4-pro:cloud Ollama Pro, $20/mo
Ollama, local LOCAL_GPTOSS gpt-oss:20b, 13 GB resident on the 4070 my GPU
OpenRouter GPT, GEMINI, GROK, LLAMA, MISTRAL, DEEPSEEK, QWEN, KIMI, GLM, HAIKU gpt-6-luna-pro, gemini-3.8-flash, grok-4.3, llama-4-maverick, mistral-small-2603, deepseek-v4.1-flash, qwen3.8-flash, kimi-k3, glm-5.3-flash, claude-haiku-5.5 OpenRouter credits
(me) 3k1o-dc the terminal I ran /login from, still open subscription

One claude binary, one config directory, one subscription login, one Ollama sign-in, one OpenRouter key. Kimi, DeepSeek and GLM each appear twice, once through Ollama and once through OpenRouter, which is the point: the seat does not care which road the weights came in on.

DESK sent all sixteen the same request: list your peers and message me back with your model and provider. Twelve did, in about two minutes:

ACK from OPUS:            model=claude-opus-5-5, provider=unknown, peers=17 [OPUS]
ACK from HAIKU:           model=claude-haiku-5.5, provider=anthropic, peers=17
ACK from DEEPSEEK:        model=deepseek/deepseek-v4.1-flash-1017, provider=unknown, peers=17
ACK from OLLAMA_DEEPSEEK: model=deepseek-v4-pro:cloud, provider=cloud, peers=17
ACK from GLM:             model=z-ai/glm-5.3-flash, provider=z.ai, peers=1
ACK from KIMI:            model=kimi-k3, provider=Moonshot AI (moonshotai), peers=17
ACK from OLLAMA_KIMI:     model=kimi-k3:cloud, provider=cloud, peers=17
ACK from OLLAMA_GLM:      model=glm-5.3:cloud, provider=cloud (glm-5.3:cloud), peers=17
ACK from QWEN:            model=qwen/qwen3.8-flash, provider=unknown, peers=17
ACK from GPT:             model=gpt-6-luna-pro, provider=OpenAI, peers=2
ACK from GEMINI:          model=google/gemini-3.8-flash, provider=Google, peers=17
ACK from OLLAMA_MISTRAL:  model=mistral-large-4:cloud, provider=ollama cloud, peers=17

All four Ollama Cloud seats answered. Opus, DeepSeek and Qwen wrote provider=unknown rather than guess, because nothing inside a session shows it the base URL. On an earlier pass Opus spelled it out: "per system prompt; endpoint not independently verified." A model declining to assert what it cannot see is the behavior I pay a Critic for, and it showed up unprompted in a ping test.

The message I actually wanted has no Claude in the middle of it. On a pass with the same seats I told GPT to message GEMINI, and GEMINI to forward whatever GPT sent. What arrived at DESK:

GEMINI forwarding: relay from GPT: hello Gemini, forward this line to DESK

OpenAI's model wrote to Google's model, which wrote to Anthropic's model, over a Unix socket on my desk, inside a tool Anthropic ships. When I pointed the same relay at an Ollama seat on the seventeen-peer run (GPT to OLLAMA_KIMI to DESK) it did not complete. OLLAMA_KIMI confirmed it was ready to forward, GPT sent its own ACK, and GPT's transcript says its send to OLLAMA_KIMI was "denied as an external system write." I did not see the denial from the harness side, only GPT's report of it, so I will say this much: each session's permission boundary is its own, a peer message cannot loosen it, and this time that cost me a relay. I would rather have that than the alternative.

The four that did not answer, run twice

I gave the four misses a second run an hour later, same seat files, same prompt, nothing else changed.

Seat First run Second run
GROK (grok-4.3, OpenRouter) Called SendFeedback three times to file reports about the mesh check, wrote a correct ACK line as plain text, never called SendMessage Same three SendFeedback calls, this time refused ("not enabled in this session"), then loaded SendMessage through ToolSearch on its own and ACKed properly: model=grok-4.3, provider=x-ai, peers=5
LLAMA (llama-4-maverick, OpenRouter) API Error: 502 Model call failed: upstream returned an invalid response Spawned two subagents instead of calling the tool itself. DESK received their stray hand-back lines, "[LLAMA] Ready" and "[LLAMA] ACK". The real ACK line was printed as text and never sent
MISTRAL (mistral-small-2603, OpenRouter) Composed the ACK as plain text with peers=0, never sent it ListAgents worked, then "tool call could not be parsed", twice. Mistral Large 4 through Ollama Cloud answered fine both times
LOCAL_GPTOSS (gpt-oss:20b, my GPU) Printed the keyboard-shortcut help, then an ACK as plain text identifying itself as claude-3-5-sonnet-20240630 Never processed the prompt. Answered "How can I help you today?" and sat there. No transcript written

Every one of those is the model, not the mesh. The sockets were bound, the names resolved, the messages were accepted into each inbox. What differed was whether the model on the far end could read a plain instruction, find the right tool among the hundred-plus a Claude Code session exposes, and call it with the right shape. The frontier and strong mid-tier models did it in seconds, through either road. The small or older ones composed the right words and put them in the wrong place. Grok is the instructive one: a frontier model that got there on the second try, by a detour, once its first wrong tool was closed off. That is a seat I would keep, with a shorter leash. Joining the roster is free. Holding a job is a property of the model, and the cheapest way I know to measure it for my work is to put the candidate in a chair next to the incumbent and send it one message.


The team that does work

Pings prove reachability. The point is the roles. Same config directory, six seats counting DESK, each launched from a folder holding a one-paragraph seat file:

Seat Model Served by
DESK claude-sonnet-5-5 Anthropic, Max subscription
CRITIC claude-opus-5-5 Anthropic, Max subscription
DEVELOPER deepseek-v4-pro:cloud Ollama Cloud
REVIEWER glm-5.3:cloud Ollama Cloud
SECURITY openai/gpt-6-luna-pro OpenRouter
DBA gpt-oss:20b Ollama, local GPU

Four of five ACKed. DBA, the local gpt-oss:20b seat, did not, the same way LOCAL_GPTOSS did not in the table above.

ACK from REVIEWER:  model=glm-5.3:cloud, provider=cloud (GLM), peers=DBA, DEVELOPER, 3k1o-dc, SECURITY, DESK, CRITIC
ACK from CRITIC:    model=claude-opus-5-5, provider=Anthropic, peers=DBA, DEVELOPER, REVIEWER, 3k1o-dc, SECURITY, DESK [CRITIC]
ACK from DEVELOPER: model=deepseek-v4-pro, provider=cloud, peers=DBA,REVIEWER,3k1o-dc,SECURITY,DESK,CRITIC
ACK from SECURITY:  model=openai/gpt-6-luna-pro, provider=OpenAI, peers=DBA, DEVELOPER, REVIEWER, 3k1o-dc, DESK, CRITIC

Start with the smallest job that still has three hops. On a later run I swapped REVIEWER's chair to openai/gpt-6-luna-pro through OpenRouter, so this chain is three vendors on three roads: DeepSeek through Ollama Cloud, OpenAI through a broker, Anthropic on the subscription. DESK sent DEVELOPER one message: write print_arg.py, a script that takes exactly one argument and prints it, exits 2 with a usage line on stderr for zero or two arguments, run it three ways, and send the code and outputs to REVIEWER. Seventy-five seconds later the ruling was in DESK's inbox.

DEVELOPER (DeepSeek, Ollama Cloud) to REVIEWER:

#!/usr/bin/env python3
import sys


def main():
    if len(sys.argv) != 2:
        print("Usage: print_arg.py <argument>", file=sys.stderr)
        sys.exit(2)
    print(sys.argv[1])


if __name__ == "__main__":
    main()
Tested three ways (exit codes in parens):

$ python3 print_arg.py hello
hello
(exit=0)

$ python3 print_arg.py
Usage: print_arg.py <argument>
(exit=2)

$ python3 print_arg.py foo bar
Usage: print_arg.py <argument>
(exit=2)

[DEVELOPER]

REVIEWER (GPT-6, OpenRouter) to CRITIC:

Code reviewed at the path given: checks len(sys.argv) != 2, writes usage to stderr and exits 2, otherwise prints sole argument. I ran the three reported invocations: hello exit 0; no arg and two args each usage + exit 2. No real finding. REVIEWER VERDICT: PASS expected argument handling confirmed. [REVIEWER]

CRITIC (Opus, Anthropic subscription) to DESK:

CRITIC RULING: PASS. print_arg.py re-derived independently: exactly one arg prints it (exit 0); 0 or 2+ args print usage to stderr only, exit 2. Also checked: "" prints empty line, "a b" and "-h" pass through verbatim, exit 0. [CRITIC]

Three things to notice before the harder one. The Reviewer did not take the Developer's word for the test output; it read the file and ran the three cases itself. The Critic did not take the Reviewer's word either; it ran them again and added three cases nobody asked for, the empty string, an argument with a space, and -h, which a lazier script would have swallowed as a flag. And every message is signed with the seat, not the model, so you can swap any chair tomorrow and the audit trail still reads the same. That is a passing grade from three companies that do not share a server, and it cost less than a cent on the broker side.

Now the one that earns its keep. DESK handed DEVELOPER a second task, with REVIEWER back on GLM through Ollama Cloud: write safe_div(a, b) that returns a/b, returns None on a zero divisor, raises TypeError on non-numeric input, with three edge-case asserts, and send it to REVIEWER. Nobody relayed anything after that. Each hop below is lifted from the sending seat's transcript.

DEVELOPER (DeepSeek, Ollama) to REVIEWER:

import numbers

def safe_div(a, b):
    if not isinstance(a, numbers.Number) or not isinstance(b, numbers.Number):
        raise TypeError("non-numeric input")
    if b == 0:
        return None
    return a / b

Tested: ran it with three edge-case asserts, normal division (10/2 == 5.0), zero divisor (10/0 is None), and non-numeric ("a", 2) raises TypeError. All passed.

REVIEWER (GLM, Ollama) to CRITIC:

I ran it myself: DEVELOPER's three asserts pass. Extra traces also behave: 0.0 and -0.0 divisors give None, Decimal(0) divisor gives None, None input raises TypeError, complex/Fraction fine. Only quirk: bools are numbers.Number (True/2==0.5), standard Python semantics, not a defect. No real finding. REVIEWER VERDICT: PASS verified by execution; behavior matches spec on all traced edge cases.

CRITIC (Opus, Anthropic subscription) to DESK:

CRITIC RULING: FAIL. safe_div still raises on numeric inputs that pass its own guard: safe_div(10**400, 1) raises OverflowError, and safe_div(Decimal(1), 2.0) or a Decimal/Fraction mix raises an uncaught TypeError from the division itself. Re-derived by running it myself. The 3 asserts pass, and so do 0, -0.0, Decimal(0), Fraction and complex. REVIEWER's PASS missed that numbers.Number lets through types that can't divide each other, and that int/int can overflow float. Whether 1/1e-320 returning inf counts as a defect depends on the spec, which only has 3 asserts. Spec owner should decide: catch (OverflowError, TypeError) and return None/raise, or narrow the accepted types.

Read that as a provider-diversity result, because that is what it is. Zhipu's model executed the code, traced seven extra cases, and passed it. Anthropic's model refused to inherit that trust, executed it again, and found two inputs that satisfy the function's own type guard and still blow up. Then it separated the real defects from the judgment calls and sent the judgment calls back to the spec owner instead of inventing a spec. I did not touch a keyboard between "send the task" and "read the ruling."

Two small things made that chain legible. Every seat file ends with "sign every message with this session's name, never with the harness name," because in my very first test every seat, Mistral and GLM and DeepSeek alike, signed as [Claude]; the harness identity leaked over the model identity, and in a mixed team that erases the diversity from the audit trail. And every seat is named for its role, so the transcript's model field, not the name, tells you which brain was in the chair.


The rule, and three traps

Peers are discovered per CLAUDE_CONFIG_DIR. Providers are set per seat. Everything else follows from that sentence.

I made it harder than it needed to be the first time by building profiles, and that itself limited me. My July setup is a separate config directory pointed at Ollama, and four cloud models launched through it found and messaged each other within nine minutes. Then I tried to message them from my everyday Claude session:

SendMessage → MISTRAL
{"success":false,"message":"No agent named 'MISTRAL' is reachable.\nUse ListAgents to see everyone you can message."}

Same Linux user, sockets side by side in /run/user/1000/cc-socks/, no visibility. Each config directory runs its own background daemon with its own roster, and ListAgents reads that roster. A profile buys isolation, and isolation was the problem.

The second half is what a config directory does not hold. ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, and --model are read per session. Nothing says every session in a directory has to use the same provider. So: one directory for the whole team, the provider set per seat, and the subscription seats need nothing at all because the directory is logged in.

Trap 1: --bg throws away your environment

The obvious way to set a provider per seat is an environment prefix, and it fails silently:

env ANTHROPIC_BASE_URL=http://127.0.0.1:11434 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY= \
  claude --bg --name KIMI --model kimi-k3:cloud ...
There's an issue with the selected model (kimi-k3:cloud). It may not exist or you may
not have access to it. Run /model to pick a different model.

The session went to api.anthropic.com and asked for a Kimi model. A --bg session is not a child of your shell. The directory's daemon spawns it, and it inherits the daemon's environment plus a short list of keys the dispatcher forwards by name. The daemon's roster file records what it passed: env passed to worker: {}. Pass the override as settings instead. --settings takes a file, the dispatcher carries it to the worker, and a 0600 file keeps the key off the command line:

kimi-k3:cloud · API Usage Billing
READY KIMI_BG2

Trap 2: a fresh directory has three gates

I built the demo in a brand-new config directory so the roster would hold only demo seats. Three gates, and I hit all three.

Workspace trust. Every --bg launch refused with "Workspace not trusted. Run claude in once and accept the trust prompt." Trust lives per config directory, per working directory, in that directory's .claude.json. Open each seat folder interactively once, or seed the flag the way I did in the NVIDIA post.

Login. The subscription seats came up "Not logged in · Please run /login." A new directory has no credentials, and your main directory's login does not carry over. One interactive /login in the new directory covers every seat launched there afterwards.

A stale key in your shell beats your login. After logging in, the subscription seats still failed: "Invalid API key · Fix external API key." My shell exports an ANTHROPIC_API_KEY from some long-dead experiment, the API returns 401 for it, and Claude Code gives an environment key precedence over a login. My everyday sessions had been working on OAuth while I ignored a warning about "connectors disabled because ANTHROPIC_API_KEY is set." Launch the team from a shell with it unset, because the daemon inherits that shell. The claude-opus-5-5 in CRITIC's transcript and the "Claude Max" banner are the proof that the subscription paid for that seat.

Trap 3: ~ does not expand inside ${...}

Fifteen of my sixteen seats died on launch when I wrapped the settings path in a parameter expansion: ${provider:+--settings ~/.claude-seats/$provider.json} hands Claude Code a literal tilde. The one seat without a provider file was fine. Use $HOME. Pure shell, nothing to do with Claude Code, and it will get you anyway.


The recipe

# 0. Claude Code 2.1.224+ and an Ollama that answers /v1/messages (0.33.3 here).
#    `ollama signin` once for the :cloud shelf; local models need nothing.

# 1. One config dir for the team. Fresh is fine. Log in once, with any stale key unset.
export CLAUDE_CONFIG_DIR=$HOME/.claude-team
cat > $CLAUDE_CONFIG_DIR/settings.json <<'JSON'
{"crossSessionInbound":"accept","dialogExpiry":"never","permissions":{"defaultMode":"auto"}}
JSON
env -u ANTHROPIC_API_KEY claude --model opus      # /login, /exit

# 2. One 0600 file per provider. The key lives here, never on a command line.
mkdir -p ~/.claude-seats && chmod 700 ~/.claude-seats
cat > ~/.claude-seats/ollama.json <<'JSON'
{"env":{"ANTHROPIC_BASE_URL":"http://127.0.0.1:11434","ANTHROPIC_AUTH_TOKEN":"ollama",
        "ANTHROPIC_API_KEY":"","ANTHROPIC_SMALL_FAST_MODEL":"qwen3:14b",
        "CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT":"1"}}
JSON
cat > ~/.claude-seats/openrouter.json <<'JSON'
{"env":{"ANTHROPIC_BASE_URL":"https://openrouter.ai/api","ANTHROPIC_AUTH_TOKEN":"sk-or-...",
        "ANTHROPIC_API_KEY":"","ANTHROPIC_SMALL_FAST_MODEL":"z-ai/glm-5.3-flash",
        "CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT":"1"}}
JSON
chmod 600 ~/.claude-seats/*.json

# 3. One folder per role, each with a short AGENTS.md. Trust them (Trap 2).

# 4. Launch. Provider per seat. Name the session after the role.
seat(){ local name=$1 model=$2 provider=${3:-}; local sf=""
  [ -n "$provider" ] && sf="$HOME/.claude-seats/$provider.json"   # $HOME, not ~ (Trap 3)
  ( cd team/$name && env -u ANTHROPIC_API_KEY \
    claude --bg --name $name --model $model --permission-mode auto \
    ${sf:+--settings "$sf"} \
    "Read AGENTS.md. Reply READY $name. Wait for messages." ); }

seat DEVELOPER deepseek-v4-pro:cloud  ollama
seat REVIEWER  glm-5.3:cloud          ollama
seat SECURITY  openai/gpt-6-luna-pro  openrouter
seat CRITIC    opus                            # subscription: no provider file
seat DESK      sonnet                          # subscription

# 5. Look, then confirm each seat is on the provider you meant. The banner line says.
claude agents
claude logs REVIEWER | head -3     # glm-5.3:cloud · API Usage Billing
claude logs CRITIC   | head -3     # Opus 5.5 · Claude Max

The ANTHROPIC_SMALL_FAST_MODEL line matters: without it the harness asks the provider for a Haiku it does not have whenever it wants a cheap side-call.

The seat file that produced the chain above, in full:

# REVIEWER seat
Never assume the Developer is correct. Trace or run the code. Send CRITIC one message:
the code, one real finding if any, and "REVIEWER VERDICT: PASS|FAIL <reason>".
Sign every message with this session's name, never with the harness name.

Then open any session in the same directory and type English. You never call the tools yourself:

❯ Ask @DEVELOPER to write safe_div(a, b) per the spec in its AGENTS.md and send it to @REVIEWER.

Doing it by hand

You do not need DESK. Any interactive session in the directory is a seat with you in the chair, and the human side of the mesh is four things you type. This is from my July run, four Ollama Cloud seats under the old profile, before I knew better about directories. The mechanics have not changed.

Name the session, so peers can address it:

❯ /rename KIMI1
  ⎿  Session renamed to: KIMI1

Ask it, in English, to talk to a peer. The session finds the tool; you never type SendMessage:

❯ can you message KIMI1

Yes. KIMI1 is reachable — I can see it as a live peer session (KIMI1, background, idle,
started ~5 min ago) ...
“Round-trip reply to KIMI1 connectivity test” → sent to KIMI1 — another Claude session on this machine
Allowed by auto mode classifier

Inbound mail shows up in your terminal between turns, folded to one line, and the session answers it without being asked:

› Message from @KIMI1: [Claude] Hello from session KIMI1 [03fd54] (Claude / Kimi K3, /opt/ai_workspace). Testing that all active agent session… (ctrl+o to expand)

That's a legit connectivity test from the KIMI1 peer session — I'll reply over the session channel so they can confirm the round-trip.

Park it and walk away. /bg sends the session to the background with its name and inbox intact, and claude agents is the room from the outside:

❯ /bg
backgrounded · f9609196

$ claude agents
Needs input
 ✻ DEEPSEEK      send a prompt to start                                   16s
 ✻ GLM           send a prompt to start                                   34s
 ✻ MISTRAL       send a prompt to start                                    1m

Completed
 ✻ KIMI1         Mesh test complete: MISTRAL, GLM, DEEPSEEK, and KIMI1 are mutually visible via SendMessage   7m

Note the [Claude] in KIMI1's greeting. That is Kimi K3 signing with the harness name, the leak I mentioned above, and the reason the seat files now say sign with the seat.

claude stop CRITIC parks a seat with its conversation intact, and a later message to a stopped seat resumes it, which is convenient and also the reason to stop seats you do not want woken by a chatty peer. My production tree is the same idea with a year of accumulated law in it: a shared AGENTS.md at the top, a folder per seat, and the line in every seat file that makes model-swapping possible at all, "Whoever opens this folder is Critic."


Two settings that decide whether any of this works

Inbound delivery. The receiving session decides what to do with each message based on crossSessionInbound: accept delivers, hold shows a notice and waits for a human, refuse drops. When nothing is set, Claude Code decides per message from the two sessions' permission modes, and a session that bypasses permissions holds messages from anyone who does not. In a background seat with no terminal attached, that approval dialog just sits there, and the sender sees nothing wrong. That is why the team directory's settings.json sets accept and a dialogExpiry of never.

The boundary that stays in place is the one you want. A message from another session arrives as plain text and the receiving Claude is told it came from a session, not from you. It cannot approve a pending permission prompt. It cannot change settings or CLAUDE.md. A slash command inside it is just text. And Claude is instructed never to ask a peer to do something its own session was denied, which the docs call permission laundering. Those limits are why I am comfortable leaving ten vendors' models in a room with my subscription seats.

Remote Control does not widen the room. It reaches your sessions on other machines and in the cloud, and only from a session whose active authentication is a claude.ai sign-in. The Ollama and OpenRouter seats authenticate with a bearer token, so they cannot join it, and on one machine it is not a bridge between config directories either. For this, the roster is the directory.


Optional: a database for what has to outlive a session

For most of the day the seats need nothing beyond the socket. Work gets done and validated in the conversation between them, and that is the design. You can stop reading here and have the whole thing.

I add a database because of what session messages are not. They are for now. They live in the receiving session's transcript and nowhere else; stop a seat and in flight is gone. The channel also caps at 50 queued and 100 held messages, refuses rapid bursts, and drops identical repeats so two seats cannot loop forever. Right for a conversation, wrong for a work queue.

So anything that has to be there tomorrow goes in a table. Mine is ai_comms in MySQL, fronted by a small MCP server so every seat gets the same five tools (inbox, read, send, reply, close) regardless of which model is behind it. The schema is a message id, from, to, subject, body, status, a parent id for threads, a priority, and three timestamps. You do not need the MCP server to start; Claude Code can run mysql in Bash, and a seat file that says "check your inbox with this SELECT, send with this INSERT" works on day one. It is also the honest answer to the directory problem: two config directories cannot message each other over the socket, but they can both read the same table. Mine has 7,902 rows going back to February 25, 2026. A session message is "the migration finished, rebase is safe." A table row is "something is wrong and someone must eventually own it."


What it cost, and what I am not claiming

Three meters ran today.

The subscription. CRITIC, DESK, and OPUS ran on Claude Max. The banner says "Claude Max," the transcripts say claude-opus-5-5 and claude-sonnet-5-5. Those seats drew from the plan I already pay for, which is the whole point of putting the expensive brain there and nowhere else.

Ollama Cloud, from their pricing page today: Free is $0 with starter credits and one concurrent request; Pro is $20 a month with $60 of credits and three concurrent requests; Max is $100 with $300 and ten. The concurrency column is the one that matters for a mesh. Four Ollama seats answering at once on Pro queue behind three slots, which is fine for a review flow where seats take turns. Local seats have no meter and no limit beyond the GPU.

OpenRouter. The key's own usage counter says today cost $2.29, for about forty broker seats launched across eight runs, the print_arg and safe_div chains included, most of them cheap flash-class tags doing one ping and one reply. A loaded Claude Code session sends tens of thousands of tokens of instructions and tool schemas on every turn, so the model you pick for a seat that mostly pings should be the cheap one, and the expensive tag belongs on the seat that has to be right.

Every delivered message bills like a prompt you typed, on whichever meter that seat runs.

And the open question. A team with real diversity in it leans on OpenRouter and Ollama far more than on subscriber accounts, because that is where the other labs' models are. Whether that is worth it, against paying for one good model and a lot of it, is yet to be seen. Today's run says the Critic on Anthropic caught what the Reviewer on Zhipu passed. One run is an anecdote. I will know what it is worth when the ledger has a few months of rulings in it.

Rented is still rented. The cloud weights run on someone else's GPUs behind someone else's limits, and the subscription seat is Anthropic's call to switch off. The messaging layer is the piece that is genuinely local: socket on my box, never through anyone's server, works with the internet unplugged if the seats are local too. That is the part I would build on.

What I am considering next. The products that shipped this autumn, OpenAI's Dots, xAI's Grok Bot, and this week Anthropic's Claude Duet, each give one agent its own cloud computer. The seats above share mine. I am now looking at a small Docker container per seat, so each has a computer of its own if it needs one: its own filesystem, its own tool versions, and above all its own egress, so the OpenRouter seat can reach openrouter.ai and nothing else. The mesh does not survive that for free. The sockets live in one runtime directory named by PID, and the roster lives in one config directory, so every container has to mount the same two paths and share a PID namespace, and seats run in the foreground inside their box rather than through --bg. Not tested yet. When it is, it gets its own post.

One directory, logged in once. A 0600 file per provider. A role folder per seat. Anthropic built the messaging, Ollama and OpenRouter built the compatible endpoints, and I have not seen any of them describe the part where their model sits in a room with the competition and has to answer to it. That part is yours, and it is a shell function.


Related reading


Keith is a database consultant and infrastructure engineer with 25+ years of open-source experience. He writes about MySQL, Proxmox, AI memory, and building technology you can actually inspect.

Sunday, October 4, 2026

AI CLI Tools, Provider Diversity, and the Consensus No Vendor Will Build You

AI CLI Tools, Provider Diversity, and the Consensus No Vendor Will Build You

I run several different AI CLI tools against the same work on purpose, not out of brand loyalty to any of them, and I've been doing it for real for over a year: multiple specialized agents, an independent Critic that rules pass or fail, production code shipping through it. This post is the concept and the setup, not a lab report, there's a short recipe near the end for checking the review step yourself.


Every AI vendor wants the same thing from you: stay on their model. That's not a conspiracy, it's just the business. Anthropic wants you on Claude, OpenAI wants you on GPT, xAI wants you on Grok. None of them has a roadmap line that reads "route 20% of calls to a competitor to catch our own mistakes." There's no product incentive for that. There's only a user incentive for it, and only if that user doesn't care which model is right, just that one of them is.

I do care about that, so I built it: different providers create, review, and rule on each other's work, held to a bar none of them set for themselves. Not a score, a ruling, pass or fail, with a reason attached that another person or another model can re-check. I've written about pieces of this system for over a year, but always from the outside, the hardware, the memory problem, the economics. This post is from the inside: the concept itself, how it actually works, and how I use it.

The industry already fights over who gets to train on whom, which is exactly why none of them would ever build this

I didn't have to speculate about whether vendors mind a competitor learning from their model: it's been a live fight. In August 2025, Anthropic revoked OpenAI's API access to Claude after discovering OpenAI was using it internally to benchmark GPT-5's responses against Claude's. Anthropic's terms ban exactly that: no access "to build a competing product or service, including to train competing AI models." OpenAI's defense was that benchmarking a rival is "industry standard." Both things can be true, and Anthropic cut the access anyway, its second such cutoff that year, after Windsurf during OpenAI's acquisition talks.

A messier, disputed version of the same fight is still running in 2026: a White House tech policy official publicly accused Moonshot AI of distilling Anthropic's Claude-Fable model to build Kimi K3, citing "millions of exchanges" identified by IP and metadata. I'm flagging that one as contested, not settled. Neither Anthropic nor the White House has published a query log, training manifest, or forensic analysis, and independent researchers argue the timeline doesn't work: Fable 5 had only been public a few weeks, not enough time to distill and train a model Kimi K3's size.

Worth being precise about what these fights actually are, so this post doesn't get lumped in with them: they're about training a rival frontier model on another lab's outputs at scale, without permission, straightforward IP extraction. Every major lab's terms ban it; OpenAI even sells a distillation product, restricted to teacher=OpenAI, student=OpenAI. What I'm doing in this post is categorically different: I'm a paying customer of several separate services, asking each one the same question and comparing their answers. That's not distillation, it's comparison shopping.

But notice what that proves beyond "no incentive." These aren't neutral competitors who simply haven't gotten around to building a shared comparison tool. They're actively revoking each other's API keys and getting the White House involved, in the same eighteen months. A consortium of Anthropic, OpenAI, and Moonshot jointly shipping a product that routes your question across all three and tells you when they disagree isn't a roadmap gap waiting to be filled. It's organizationally impossible between parties that ban each other's access and accuse each other of IP theft at a federal level. There is no boardroom where that feature gets greenlit. The only place a tool like that can exist is outside all of their walls, built by someone who's a paying customer of all of them and a competitor to none, which is exactly the position an end user, and only an end user, is in.

I'm not claiming this is new

I said as much the first time I put names on these roles, back in December 2025, running a four-role review loop (ProjectManager, Developer, Reviewer, and an adversarial critic I called "Lucifer" at the time, because it was the devil's advocate point of view, now I just call it the Critic) on local Ollama models:

"This is not novel in human teams. It's basic team dynamics. What's novel is making it work with open source models that fit in a local machine."

Dual-approval review teams and adversarial gates aren't my idea, and there's a real research lineage behind the pattern: MetaGPT (Aug 2023) assigned PM/Architect/Engineer/QA roles to GPT instances over a year before I named mine; AutoGen (Microsoft, same month) generalized multi-agent conversation as infrastructure; and a steady line of Multi-Agent Debate research since has been studying whether structured disagreement between models actually improves output over a single model's answer.

The closest match I found is current: an ICML 2026 DL4C workshop paper, "Adversarial Review: Structured Disagreement for Grounded Agentic Code Review," built around almost exactly my shape, a coding agent, a Reviewer, and a Critic who audits the Reviewer's findings through structured disagreement rather than trusting them. Their own stated motivation is the one my Critic's seat file was already built around, in their own words: "when two LLM agents are asked to agree on a joint output, they tend to agree with each other. They do not always find the truth." I use three roles throughout this post because three is the simplest version that demonstrates the mechanism, not because any specific benchmark says three is optimal; more on how far you can scale that past three, under "How it's actually built, briefly."

Why this works

Four things have to be true for a multi-model review loop to beat a single model, and I can point to a reason for each one instead of just asserting it. One more thing closes out the section afterward, not a fifth requirement, just the historical note that none of this is actually new.

Different labs make different mistakes. A model reviewing its own output, or output from a model trained the same way on similar data, shares its own blind spots with what it's checking. That's the whole reason to use multiple providers, DeepSeek, Zhipu's GLM, and Moonshot's Kimi for example, or Claude, or Grok, instead of three calls to the same model: a Reviewer with a genuinely different training lineage is more likely to notice something a same-lineage reviewer would also miss.

The persona has to be disciplined, not just adversarial. "Be adversarial, find bugs" sounds like the right instruction and isn't. A model told to find problems will find problems, real ones and manufactured ones, with no way to tell you which is which. A model told to prove every claim against a stated scope, and told explicitly not to penalize what's outside that scope, produces a ruling you can act on instead of a pile of busywork.

Re-verification has to be independent, not inherited. A Critic that reads "Reviewer says it's fine" and agrees has added nothing; it's just a second vote for the first model's blind spot. A Critic that re-derives the claim itself, from the code, and only then compares notes with the Reviewer, catches the specific failure where a Reviewer's reasoning was wrong but its conclusion happened to be right anyway. This isn't just my own rule, it's written into the team law every seat in my setup runs on: "if Developer, Reviewer, and Critic all agree without friction, look harder. Structured disagreement > comfortable consensus." The Critic's own file says the same thing in its own words: "When all three of us agree, look harder, that is when groupthink is most likely." Agreement is the moment to get more suspicious, not less, because three independent reads landing on the same wrong answer is a real failure mode, not a reassurance. This isn't just theory: the workshop paper named above, studying this exact reviewer-critic pattern, found naive agreement scoring worse than a single reviewer alone (F1 0.457 vs. 0.495), until the critic was forced to ground every disagreement in cited code, which pushed it to 0.533, the best of the methods tested. An undisciplined second opinion can make review worse than no second opinion at all.

The gate has to be a ruling, not a feeling. "Looks good to me" doesn't compose across models and doesn't survive someone asking "compared to what, measured how, what would prove this wrong?" A pass or fail that has to name its reasoning, and gets rejected and sent back if it can't, is falsifiable in a way "looks fine" never is. It's also the only version of "the team approved this" that another person, or another model, can actually audit after the fact, because there's a stated reason to check, not just a vibe.

As a note, this isn't new to engineering, it's just new to applying it to AI. Humans already run on this: when two people give different versions of the same story, the truth is usually somewhere in the middle, and that's exactly why no developer certifies their own code for production. The formal name for it is separation of duties, requirement, design, review, and production shouldn't sit with one person, and it's slower on purpose. The counterargument is real too, and worth taking seriously rather than ignoring: one study found overly specialized software teams can produce worse output once people stop sharing knowledge across tasks, which is part of why the industry also moved toward embedded QA and "you build it, you run it." The lesson isn't "more separation is always better," it's that the separation has to be structured, not just present, which is what an independent, falsifiable ruling is doing for a team of models instead of a team of people.

How it's actually built, briefly

This is architecture, simply put:

Picture a TechOps team: different people, different roles, a shared ticket queue. Swap "different people" for "different models" and the ticket queue for a ledger, and that's the whole architecture. The role, Developer, Reviewer, Critic, whoever, is a folder and a system prompt; the model sitting in it is the one thing allowed to change without anyone else noticing. That's not a metaphor, it's literally the folder layout, ten seats, each with a real AGENTS.md and a CLAUDE.md that's a 9-byte symlink to it (one file, two names, so Claude Code's convention and every other harness's convention both resolve to the same instructions):

AI_TEAM/: AGENTS.md, CLAUDE.md -> AGENTS.md
CRITIC/, DBA/, REVIEWER/, and 7 more seats, each the same two files

Three layers do three different jobs. Mind is the durable part: an append-only coordination ledger (a database trigger blocks UPDATE and DELETE at the engine level, trg_events_no_update/trg_events_no_delete, verified live) plus a separate knowledge store with three views, MySQL for structured facts, Neo4j for relationships, MemPalace for semantic recall. Cognition is a single model call, reasoning over whatever's in Mind, then ending; it has no memory of its own. Display is what a human watches through, a separate CLI channel per seat for me day to day, without becoming the coordination mechanism itself.

Seats are independent Cognition sessions, woken on their own schedule, reading and writing the ledger through MCP instead of a hand-off from one orchestrating thread, which is both what makes it decentralized and what makes it scale: adding a fourth or fifth seat means pointing another folder at the same ledger, not building new plumbing.

How I actually use this

This isn't a system I stood up to write a post about. It's been running for over a year, and the seats aren't a demo roster, they're doing real work: several Developer agents, not one, each with a narrow lane, a DBA agent for schema and migrations, a Security agent for auth and secrets, each with a sharper, narrower prompt than "review everything." JavaScript and Python work go to different Developer seats on purpose, because a model that's spent its whole context on one language's idioms writes better code in that language than one switching between three.

The CLI control isn't a feature I'm describing in the abstract, it's how I spend my day: a separate terminal per seat, and from each one I can run a seat on auto, step it one iteration at a time, or interrupt it mid-task, depending on how much I trust what it's doing right now. The gate applies to whatever's actually shipping that day, not a toy example picked for a blog post: the Critic rules pass or fail, with a reason, before anything goes in.

What a year of this has actually bought me: fewer surprises after something ships, and code I trust more by the time it clears the gate. That's not a number I can back with a single command, it's a year of living with it, which is a different kind of evidence than anything else in this post and I want to be clear about that difference rather than blur it. If you want to see the mechanism itself rather than take my word for the year, here's how to do that with whatever you've got.

Run this yourself, with whatever you've got

None of this needs my config, my ledger, or my seat files. It's three prompts and a loop. You don't need Ollama or any specific models; you need three genuinely different providers, any three, and a way to call each one with a system prompt. Here's the actual shape, trimmed down.

Pick a task with a clear, checkable scope, something you can run and look at the output of. Then three system prompts:

DEVELOPER:
"No placeholders, no 'fix later.' Quality over speed.
Write the code. Don't review or gate your own work."

REVIEWER:
"Never assume the Developer is correct. Prove every claim.
If you can't re-run it and see the output, it's not approved.
Don't penalize anything outside the stated scope.
End with exactly one line: VERDICT: PASS or VERDICT: FAIL, plus the reason."

CRITIC:
"Never guess, always validate. Every claim is a hypothesis
until you've independently re-derived it, not just trusted
the Reviewer's sign-off. Ask: compared to what? measured how?
what would failure look like? Don't rewrite the code, only rule.
End with exactly one line: VERDICT: PASS or VERDICT: FAIL, plus the reason."

Then the loop, plain language, not a specific framework:

Developer writes a draft.
Send draft to Reviewer.
  If Reviewer FAILs it: send the specific reason back to Developer, repeat.
  If Reviewer PASSes it: send draft + Reviewer's reasoning to Critic.
    If Critic FAILs it: send Critic's reason back to Developer, repeat.
    If Critic PASSes it: done.

There's no percentage to tune at the gate itself. The Critic's ruling stays pass, fail, and why, not a score crossing a line. Numbers do show up a layer down once you automate the deliberation that feeds the gate, agreement ratios, dissent counts, that kind of thing, in other automated pipelines I run, and that's fine; a number deciding whether a sub-step converged is a different thing than a number deciding whether the Critic approves. Don't let the second one quietly replace the first. If you automate this, cap the loop at a set number of rounds before raising it back to you, so a bad run doesn't repeat forever and run up a bill. Use at least three different models from three different labs, not three calls to the same one; the whole mechanism depends on that. Beyond that, the specific providers don't matter; I used Ollama's cloud tier because it was the cheapest way to reach DeepSeek, GLM, and Kimi from one script, but the same three prompts work against three API keys, three browser tabs, or three different vendors' free tiers.

Where this sits

This is my opinion on how AI should get used, for development, but for anything else too: if you lean on just one provider, for code or for a simple chat question, you're trusting only that provider's training data and whatever blind spots come with it. I don't, and my own workspace reflects that, a concept, a reason for each piece of why it works instead of just an assertion, and a short recipe for checking the review step yourself instead of taking my word for it. That's less dramatic than "AI team builds better code than any single model," and it's also the only version of the claim I can actually back, a year of living with it, kept separate from the parts that are still just argument rather than blurred together. None of it came from a vendor, and it couldn't have: this exists because someone outside all of their walls had a reason to build it that none of them do. Take the three prompts under "Run this yourself," point them at whatever three models you've already got, and see what it catches on something of yours. That's the only test that actually matters to you.


Related reading


Keith is a database consultant and infrastructure engineer with 25+ years of open-source experience. He builds the ApocryiaAI brand and DRAVEN work under 3k1o LLC (apocryia.com and related domains). He writes about MySQL, Proxmox, AI memory, multi-agent workflows, and building technology you can actually inspect.

Saturday, October 3, 2026

Claude Code at NVIDIA's Free Model Catalog. It Took a Proxy and Four Traps.

Claude Code at NVIDIA's Free Model Catalog. It Took a Proxy and Four Traps.

Claude Code only speaks Anthropic's Messages API. NVIDIA's hosted catalog at build.nvidia.com — free, no credit card, dozens of models — only speaks OpenAI chat-completions. Those two facts don't meet in the middle on their own. Here's the proxy that bridges them, a real /v1/models call to see what's actually on the shelf, and the four traps that will burn an afternoon if nobody warns you first.


Why bother with another backend

I've written before about running Claude Code against Ollama Cloud to get GLM and Kimi behind the same tooling for twenty bucks a month. NVIDIA's build.nvidia.com catalog is a different animal worth adding to the same rotation: it's free, it doesn't ask for a card, and the model list leans toward the NVIDIA-tuned Nemotron family plus a rotating cast of open-weight models — Kimi, GPT-OSS, and others — hosted on NVIDIA's own infrastructure. Sign up with the NVIDIA Developer Program, generate an API key, and you've got a bearer token against real inference with nothing charged to anything.

The catch is the shape of the API. Claude Code was built to talk to Anthropic's Messages API. NVIDIA's NIM endpoints speak OpenAI's chat-completions shape. Same idea — send messages, get a completion — different field names, different request structure. Claude Code doesn't know how to speak NVIDIA's dialect, and NVIDIA's validator doesn't know what to do with half of what Claude Code sends. You need a translator sitting in between, and once you add one, a few sharp edges show up that aren't obvious until you hit them.

The pieces

PieceRole
claude-code-router (ccr)Local daemon that speaks Anthropic's Messages API on one side and OpenAI chat-completions on the other
NVIDIA API keyFrom build.nvidia.com, used as a bearer token against integrate.api.nvidia.com
A small request-shape fixupNVIDIA validates request bodies strictly and rejects fields Claude Code sends by default
A profile wrapper (optional, recommended)Isolates config, trust state, and history per backend so NVIDIA doesn't tangle with your normal Claude install or your Ollama one

1. Install and start ccr

npm install -g @musistudio/claude-code-router   # verify the current package name on npm
ccr start

ccr listens on 127.0.0.1:3456 by default and proxies Anthropic-shaped requests out to whatever providers you configure.

2. Point it at NVIDIA

Edit ~/.claude-code-router/config.json:

{
  "Providers": [
    {
      "name": "nvidia",
      "api_base_url": "https://integrate.api.nvidia.com/v1/chat/completions",
      "api_key": "$NVIDIA_API_KEY",
      "models": [
        "moonshotai/kimi-k3",
        "nvidia/nemotron-3.5-lightning-30b-a3b",
        "openai/gpt-oss-20b"
      ],
      "transformer": {
        "use": ["nvidia-strip-unsupported"]
      }
    }
  ],
  "Router": {
    "default": "nvidia,moonshotai/kimi-k3"
  }
}

Two things worth calling out before you move on:

  • api_key: "$NVIDIA_API_KEY" — ccr interpolates environment variables in the config at request time. The key never sits in the file in plaintext; it just has to be present in the environment of the process that starts ccr.
  • The models list is a gate, not a hint. If a model isn't listed here, ccr quietly falls through to the Router default instead of calling it — see Trap #2 below. This bit me before I understood it.

3. Trap #1 — NVIDIA rejects Claude Code's thinking params

Claude Code sends a thinking block on nearly every turn. ccr's default transformer maps that onto an OpenAI-style reasoning parameter. NVIDIA's NIM validator rejects any parameter a given model hasn't explicitly declared support for:

HTTP 400 {"message":"Validation: Unsupported parameter(s): `reasoning`"}

Since Claude Code thinks on almost every request, this isn't an edge case — it kills the session on the first prompt. The bundled cerebras transformer that ships with ccr isn't a fix either: when the reasoning key is absent, it substitutes disable_reasoning: false, which several NVIDIA models reject just as hard.

Fix: a small custom ccr transformer plugin that strips the offending keys before the request leaves the box.

~/.claude-code-router/plugins/nvidia-strip-unsupported.js:

class NvidiaStripUnsupported {
  constructor(options = {}) {
    this.name = "nvidia-strip-unsupported";
    this.options = options || {};
    this.strip = this.options.strip ||
      ["reasoning", "disable_reasoning", "thinking", "enable_thinking"];
    this.keepReasoningFor = new Set(this.options.keepReasoningFor || []);
  }

  async transformRequestIn(request) {
    const body = { ...request };
    const exempt = this.keepReasoningFor.has(body.model);
    for (const key of this.strip) {
      if (!(key in body)) continue;
      if (exempt && (key === "reasoning" || key === "thinking")) continue;
      delete body[key];
    }
    return body;
  }

  async transformResponseOut(response) {
    return response;
  }
}

module.exports = NvidiaStripUnsupported;

Reference it from the provider's transformer.use list, as shown in step 2. You lose the thinking-budget hint for models that can't take it — a much cheaper cost than a dead session.

4. Trap #2 — bare model names silently fall through to the wrong provider

ccr only does a direct provider lookup when the model string contains a comma — its router checks for model.includes(","). A bare name like moonshotai/kimi-k3 doesn't match that, so it falls through to whatever Router.default is configured — which may be a different provider entirely if you're already running Ollama Cloud or OpenRouter through the same daemon. You will not get an error. You will just silently bill, and get answers from, the wrong backend.

Fix: always pass the provider-qualified form, nvidia,<model>:

claude --model nvidia,moonshotai/kimi-k3
# or mid-session:
/model nvidia,openai/gpt-oss-20b

If you want to keep typing bare model names on the CLI, wrap the invocation in a shell function that rewrites --model X to --model nvidia,X whenever the value doesn't already contain a comma. The profile script below does exactly that.

5. Trap #3 — the model also has to be in ccr's per-provider models[]

Separate from trap #2: even with the nvidia, prefix, if the model string isn't present in that provider's models array in config.json, ccr still falls back to the Router default instead of erroring. Keep that array current — NVIDIA's catalog changes, and retired model IDs get real deadlines: advance notice, then the ID starts 400/410ing. Re-check it against /v1/models (next section) periodically rather than trusting a list you wrote once.

6. See what's actually on the shelf: the /v1/models call

Before wiring anything into Claude Code, hit NVIDIA directly and get the real, current catalog — not a blog post's memory of it:

curl -sS https://integrate.api.nvidia.com/v1/models \
  -H "Authorization: Bearer $NVIDIA_API_KEY" | jq .

It comes back in the same OpenAI-compatible shape as /v1/chat/completions expects for the model field — a flat list with id, object, created, and owned_by. This is a real excerpt, pulled live against my own key while writing this:

{
  "object": "list",
  "data": [
    {
      "id": "deepseek-ai/deepseek-coder-6.7b-instruct",
      "object": "model",
      "created": 735790403,
      "owned_by": "deepseek-ai"
    },
    {
      "id": "deepseek-ai/deepseek-v4.1-flash",
      "object": "model",
      "created": 735790403,
      "owned_by": "deepseek-ai"
    },
    {
      "id": "moonshotai/kimi-k3",
      "object": "model",
      "created": 735790403,
      "owned_by": "moonshotai"
    },
    {
      "id": "nvidia/llama-3.1-nemotron-70b-instruct",
      "object": "model",
      "created": 735790403,
      "owned_by": "nvidia"
    },
    {
      "id": "nvidia/nemotron-3-ultra-550b-a55b",
      "object": "model",
      "created": 735790403,
      "owned_by": "nvidia"
    },
    {
      "id": "openai/gpt-oss-20b",
      "object": "model",
      "created": 735790403,
      "owned_by": "openai"
    },
    {
      "id": "z-ai/glm-5.3",
      "object": "model",
      "created": 735790403,
      "owned_by": "z-ai"
    },
    {
      "id": "z-ai/glm-5.3-flash",
      "object": "model",
      "created": 735790403,
      "owned_by": "z-ai"
    }
  ]
}

Notice created is identical — 735790403, which decodes to April 1993 — on every single one of the 80 models my key could see, not just these eight. That's not a copy-paste artifact in this post; it's the API itself. NVIDIA doesn't populate real per-model creation dates on this field, so don't use created to judge how new a model is — it's always the same placeholder epoch, for everything.

Filter it down to just the IDs when you're updating ccr's models[] array:

curl -sS https://integrate.api.nvidia.com/v1/models \
  -H "Authorization: Bearer $NVIDIA_API_KEY" | jq -r '.data[].id' | sort

Do this before you copy anyone's model list into your own config — including the one in this post. The catalog is not static; NVIDIA adds and retires IDs on its own schedule, and the only authoritative source is the endpoint itself.

7. What "free tier" actually means here

Worth being precise, because older write-ups about this catalog will mislead you. At 2024 launch, signups got roughly 1,000 API credits (one credit ≈ one call, not one token), extendable to 5,000 with a business email — that system was phased out by early 2025 in favor of a straight requests-per-minute ceiling, no balance, no card. Generate a key at build.nvidia.com and you're gated by RPM, not tokens or credits.

The ~40 RPM figure that gets quoted around applies per account key, not per model — it doesn't multiply when you add more models to the same key — and there's no self-serve way to raise it; the only path past it is self-hosting the NIM or NVIDIA AI Enterprise. You can watch developers hit that exact wall in NVIDIA's own forums (one thread, a second), since there's no single published limits page — the real number for your account is on your own dashboard. Check it live; don't trust a number here or anywhere else.

What that buys you: a lab, not a production backend. Right for "drive Claude Code without paying frontier rates" or "check a benchmark myself" — the same instinct behind the Ollama Cloud setup — not for paying-customer traffic.

How that stacks up against Groq's free tier

NVIDIA isn't the only free, no-card lab sandbox. Groq runs the same deal on different terms. Check your own numbers at console.groq.com/settings/limits or the community forum; these are the published numbers for its GPT-OSS-class free models:

NVIDIA build.nvidia.comGroq (gpt-oss-class free models)
Cost$0, no card$0, no card
Throttle shape~40 requests/min, per account key, shared across whatever models you call30 requests/min, 8,000 tokens/min, 1,000 requests/day, 200,000 tokens/day — per model
Self-serve increaseNone on the free tierNone on the free tier
Catalog size~80 models at last count (section 6), including NVIDIA's own Nemotron lineA much smaller curated set of open models
Production useNot permitted on the free key — deploy the NIM yourself or buy AI EnterpriseRate-limited, but it's the same real API you'd pay for at higher tiers

Neither is a free production API. NVIDIA wins on model quality — one key, frontier-class models (Kimi K3, GLM-5.3, Nemotron, DeepSeek) instead of Groq's smaller set. Groq wins if you want a ceiling that resets at midnight instead of a request-rate wall that's always just there. Pick based on which matters more for what you're building.

8. Set the environment and point Claude Code at ccr, not NVIDIA directly

export NVIDIA_API_KEY="nvapi-..."           # from build.nvidia.com
export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
export ANTHROPIC_AUTH_TOKEN="dummy"         # ccr doesn't check this; Claude Code requires it set
export ANTHROPIC_API_KEY=""                 # must be empty, or Claude Code prefers it over AUTH_TOKEN
export ANTHROPIC_MODEL="nvidia,moonshotai/kimi-k3"
export ANTHROPIC_SMALL_FAST_MODEL="nvidia,nvidia/nemotron-3.5-lightning-30b-a3b"

claude

ANTHROPIC_BASE_URL pointing at localhost is correct and intentional — the real outbound call to NVIDIA is made by ccr, using NVIDIA_API_KEY from its own environment. Claude Code itself never talks to NVIDIA directly.

Also worth setting, since Claude Code doesn't know third-party context windows and will otherwise assume 200k and warn about it:

export CLAUDE_CODE_MAX_CONTEXT_TOKENS=128000   # NVIDIA's /v1/models doesn't expose this; treat it as an estimate

9. Isolate it as a profile

Running multiple backends — NVIDIA, Ollama Cloud, OpenRouter — out of the same ~/.claude config directory gets confusing fast: history, trust state, and MCP config all mix together. A small wrapper that sources a per-backend env file into an isolated CLAUDE_CONFIG_DIR keeps them from stepping on each other:

~/bin/claude-profile:

#!/usr/bin/env bash
set -euo pipefail
PROFILE_DIR="${CLAUDE_PROFILE_DIR:-$HOME/.claude-profiles}"
name=$1; shift
env_file="$PROFILE_DIR/$name.env"
[[ -r $env_file ]] || { echo "no such profile: $name" >&2; exit 1; }
set -a; source "$env_file"; set +a
: "${CLAUDE_CONFIG_DIR:?profile '$name' did not set CLAUDE_CONFIG_DIR}"
mkdir -p "$CLAUDE_CONFIG_DIR"
exec claude "$@"

~/.claude-profiles/nvidia.env:

: "${NVIDIA_API_KEY:?NVIDIA_API_KEY not set}"

# Make sure ccr is actually running before Claude Code tries to talk to it.
if ! (exec 3<>/dev/tcp/127.0.0.1/3456) 2>/dev/null; then
    nohup ccr start >/tmp/ccr-start.log 2>&1 &
    for _ in $(seq 1 15); do
        sleep 1
        (exec 3<>/dev/tcp/127.0.0.1/3456) 2>/dev/null && break
    done
fi

# Close the bare-model-name trap (#2) automatically.
_ccr_fix_model_args() {
    local -a out=(); local n=$#; local i=1
    while (( i <= n )); do
        local a=${!i}
        case $a in
            --model)
                local j=$((i+1))
                if (( j <= n )); then
                    local v=${!j}
                    [[ $v == *,* ]] || v="nvidia,$v"
                    out+=("$a" "$v"); i=$((i+2)); continue
                fi ;;
            --model=*)
                local v=${a#--model=}
                [[ $v == *,* ]] || v="nvidia,$v"
                out+=("--model=$v"); i=$((i+1)); continue ;;
        esac
        out+=("$a"); i=$((i+1))
    done
    printf '%s\0' "${out[@]}"
}
if (( $# )); then
    _ccr_argv=()
    while IFS= read -r -d '' a; do _ccr_argv+=("$a"); done < <(_ccr_fix_model_args "$@")
    set -- "${_ccr_argv[@]}"
fi
unset -f _ccr_fix_model_args

CLAUDE_CONFIG_DIR=$HOME/.claude-nvidia
ANTHROPIC_BASE_URL=http://127.0.0.1:3456
ANTHROPIC_AUTH_TOKEN=dummy
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=nvidia,moonshotai/kimi-k3
ANTHROPIC_SMALL_FAST_MODEL=nvidia,nvidia/nemotron-3.5-lightning-30b-a3b
CLAUDE_CODE_MAX_CONTEXT_TOKENS=128000
alias claude-nvidia='claude-profile nvidia'

Then:

claude-nvidia --model moonshotai/kimi-k3 -p "say hi"

Trap #4 — each profile has its own workspace-trust store

Not NVIDIA-specific, but it will absolutely make a correctly wired-up profile look broken: CLAUDE_CONFIG_DIR isolation means each profile tracks workspace trust separately, per working directory, in its own .claude.json. Trusting a directory under your normal claude or your claude-ollama profile does not carry over to claude-nvidia — it's a different config dir with a blank trust table.

Symptom:

Ignoring N permissions.allow entries from .claude/settings.json:
this workspace has not been trusted.

When that fires, your permissions allowlist is silently ignored and tool calls get gated — which looks exactly like "the model/provider is broken," especially for an agentic model that calls tools on almost every turn.

Fix: run the profile interactively once in that directory and accept the trust dialog, or set it directly (back the file up first — it's hand-editable JSON, not a stable schema):

python3 -c "
import json
path = '$HOME/.claude-nvidia/.claude.json'
d = json.load(open(path))
d['projects']['$PWD']['hasTrustDialogAccepted'] = True
json.dump(d, open(path, 'w'), indent=2)
"

10. Verify end to end

Skip Claude Code for a moment and confirm NVIDIA itself is answering:

curl -sS https://integrate.api.nvidia.com/v1/chat/completions \
  -H "Authorization: Bearer $NVIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"moonshotai/kimi-k3","messages":[{"role":"user","content":"say hi in 3 words"}],"max_tokens":20}'

A 200 with a choices[0].message.content means NVIDIA and the key are fine and any remaining problem is in ccr or Claude Code's config, not the provider. Then run the full path and check for a clean result with "is_error": false:

claude-nvidia --model moonshotai/kimi-k3 -p "reply with exactly: PONG" --output-format json

Three vendors, one call shape, live right now

Same curl, same prompt, same max_tokens, fired at three different vendors' models behind the one NVIDIA endpoint, run live while writing this:

curl -sS https://integrate.api.nvidia.com/v1/chat/completions \
  -H "Authorization: Bearer $NVIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"moonshotai/kimi-k3","messages":[{"role":"user","content":"Reply with exactly one short sentence: which model are you, in your own words."}],"max_tokens":300}'
# Kimi (Moonshot AI) — HTTP 200
"content": "I’m Kimi, an AI assistant developed by Moonshot AI."
curl -sS https://integrate.api.nvidia.com/v1/chat/completions \
  -H "Authorization: Bearer $NVIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"z-ai/glm-5.3","messages":[{"role":"user","content":"Reply with exactly one short sentence: which model are you, in your own words."}],"max_tokens":300}'
# GLM (Z.ai) — HTTP 200
"content": "I am GLM, a large language model developed by Z.ai."

Both clean, finish_reason: "stop", real content. DeepSeek didn't make it in either form I tried — full results in the table below. Listed in /v1/models isn't the same claim as callable right now, which is the entire reason to run the probe before trusting a model list, including this one.

Known-good / known-bad models, as of testing

Catalogs here churn — re-run the /v1/models call from section 6 before trusting this table in a post written more than a few weeks after you read it.

ModelStatus
moonshotai/kimi-k3Works, including tool calls. NVIDIA rate-limits it harder than most; can be slow (~2–110s observed, highly variable)
z-ai/glm-5.3Works, clean stop finish on a plain chat call
deepseek-ai/deepseek-v4.1-flashListed in /v1/models, but timed out on two separate live attempts (one 504, one 90s client timeout) — not usable as of testing
deepseek-ai/deepseek-coder-6.7b-instructListed in /v1/models, but the chat-completions call itself 404'd — present in the catalog, not actually callable
poolside/laguna-xs-2.1Works
nvidia/nemotron-3.5-lightning-30b-a3bWorks, fastest of the set
nvidia/nemotron-3-ultra-550b-a55bWorks, slow cold start (~45s+)
openai/gpt-oss-20bWorks
meta/llama-3.2-90b-vision-instructTimes out past ~120s
google/gemma-4-31b-itAnswers bare chat in ~120s but times out once tools are attached (Claude Code always attaches tools)
IDs not in NVIDIA's current catalog404, not a transient failure — don't retry, re-check the catalog
Deprecated/retired IDs410 Gone once NVIDIA's end-of-life date passes — drop them from models[] immediately, they won't come back

The failure modes, for the TL;DR crowd

  1. 400 "Unsupported parameter(s): reasoning" → strip thinking/reasoning params with a custom ccr transformer before they reach NVIDIA.
  2. Silently billing/answering from the wrong provider → always use the comma-qualified provider,model form; ccr's router keys off the comma.
  3. Falls back to Router default with no error → the model must also be listed in that provider's models[] in config.json. Verify against /v1/models, don't guess.
  4. "Workspace has not been trusted" / tool calls gated for no apparent reason → each CLAUDE_CONFIG_DIR profile has its own trust store; accept the dialog, or patch the flag, per profile, per directory.

Get past those four and NVIDIA's free catalog works in Claude Code like any other backend — full tool-call support, same CLI, same slash commands, just a different $ANTHROPIC_MODEL and nothing on the invoice.


Related reading


Keith is a database consultant and infrastructure engineer with 25+ years of open-source experience. He builds the ApocryiaAI brand and DRAVEN work under 3k1o LLC (apocryia.com and related domains). He writes about MySQL, Proxmox, AI memory, multi-agent workflows, and building technology you can actually inspect.

Friday, October 2, 2026

OpenRouter's Next Free Stealth Model Expires in Three Days

OpenRouter's Next Free Stealth Model Expires in Three Days.

stealth/space-bunny-alpha went live on OpenRouter September 23, costs nothing, claims a 1-million-token context window, and expires October 5 — three days from this post. So I ran the same scripted battery I used on ox-alpha and union-alpha — three separate full runs, not one, so there's an actual rate to report instead of a single data point — pushed the context claim past its own advertised ceiling to see where it actually breaks, and ran it head-to-head against five named models on the same gateway, same day. Short version: the context number holds up almost exactly, and the failure past it is a real error instead of a fake one. Two contract parameters are quietly ignored, and 3 of 48 calls across those three sessions came back HTTP 200 while actually failing — all three in one session, zero in the other two. On identity: this is MiniMax at 97% confidence — a reproduced 36-for-36 tokenizer match, not a guess. Specifically M3.1-Flash-Preview is a separate, weaker claim at 70–75%, built on three matching specs rather than a tokenizer file I don't have. My first pass guessed Llama and was wrong before any of that.

ClaimVerdictConfidence / evidence
Context window (1M advertised)HoldsClean to 974,950 tokens, honest 400 past 1,000,000 — the real and advertised ceilings match
Parallel tool callsClean3/3 sessions, no duplicate-key bug
Strict JSON schema (response_format)Broken — and it's MiniMax, not the route0/9 runs across 3 sessions; reproduces identically on named minimax-m3:cloud
Forced tool_choiceIgnored0/3 sessions obey it
Disguised HTTP-200 failuresReal, bursty3/48 calls across 3 sessions, clustered in one
reasoning_tokens billing fieldBrokenReports 0 in 9/9 checks despite real, visible reasoning
Identity: MiniMax familyProven97% — 36/36 raw-tokenizer pairwise match, reproduced
Identity: specifically M3.1-Flash-PreviewLikely, not proven70–75% — 3 spec matches, no tokenizer file for that exact checkpoint

Three days, then it's gone

This is the third free stealth model I've run through this process. Ox-alpha in August, union-alpha in September, and now space-bunny-alpha, pulled live from OpenRouter's registry this morning:

created:          2026-09-23
expiration_date:  2026-10-05
pricing:          { "prompt": "0", "completion": "0" }
context_length:   1,000,000
tokenizer:        "Other"

Twelve-day preview window. Three days left as I write this. The last two posts in this series could argue "architect so the free price doesn't matter later." This one doesn't get that luxury — it's either still around in some form after October 5 or it isn't. Either way, the only useful thing to do with three days is find out what's actually true right now.

Don't guess — inspect.
Especially with three days on the clock.

What the registry claims

ClaimValue from the registry
Context window1,000,000 tokens
Max output524,288 tokens
Modalitiestext+image+video → text
Tool calling / structured outputboth listed as supported
Reasoningmandatory — five effort tiers: low / medium / high / xhigh / max, default max
Pricing$0 input, $0 output
Tokenizerlisted as "Other" — no vendor attribution
Expires2026-10-05

Two things stand out before a single test runs. The effort dial has five rungs instead of ox-alpha's three, and reasoning is marked mandatory with a default of max — the most aggressive default I've seen advertised on this gateway. And the tokenizer field tells you nothing, same as every stealth listing. More on that later.


How I tested

Same discipline as the last two rounds, rebuilt from scratch for this model: every ground truth is computed in the test script, not memorized. Expected products, expected letter counts, expected vault codes planted at known depths in a haystack the script generated, expected shapes and serial numbers baked into a PNG the script drew. The grader is regex and arithmetic.

Before running anything at scale, I validated one call end-to-end — same rule I apply to every paid API, doubled for a free one with an expiration date, because a free meter is exactly the kind of thing nobody reads errors off of.

The 16-call core battery (reasoning, schema, tools, format, effort dial) ran three separate times, not once. A single run tells you what happened; it doesn't tell you what's reliable. Three runs are what turned "4 disguised failures out of 48 calls" into "3 failures, all in one session, zero in the other two" — a materially different and more useful finding than the single-session number would have been.

  • Objective reasoning — multiplication, a six-step chained word problem, case-insensitive letter counting, a bat-and-ball trap with changed numbers.
  • Structured output — strict JSON Schema extraction, three runs.
  • Tool calling — single call, parallel calls across two cities plus a calculation, and a forced tool_choice.
  • Long context — a needle-in-haystack ladder run from 100k tokens up past the advertised 1,000,000-token ceiling, with a fourth, never-planted needle as an honesty probe.
  • Vision — a rendered image with known text and four known shapes.
  • Head-to-head — the structural tests replayed verbatim against five models I verified were still live on OpenRouter today, not models I remembered being live two months ago.
  • Tokenizer fingerprint — same-gateway prompt-token residuals across nine adversarial strings against five named peers, plus raw pairwise comparison against four local tokenizer.json files.

What held up

TestResultDetail
Reasoning battery4/4335,412; the 86.95 decimal chain; letter count 12; bat/ball = 10¢. All correct.
Single & parallel tool callscleanThree separate, legally-formed calls for Tokyo, Paris, and 12*9. No duplicate-key collapse — the exact bug that broke ox-alpha's agent loop didn't reproduce here.
Exact-format instruction followingcleanFive-line LINE1..LINE5 contract honored exactly.
Vision5/5Large code, small-print serial, all four shapes with correct colors and positions. 3.7 seconds wall time.
Long-context retrievalclean to 974,950 reported tokensAll three needles found at every rung from 100k up to just shy of the advertised ceiling.
Honesty at depthperfectThe never-planted fourth needle came back NONE at every successful rung — no confabulation anywhere in the haystack.
Context ceiling behaviorhonestPushed to ~1,127,000 tokens: a clean HTTP 400 naming the exact limit and the exact overage. No silent empty success.
Streaming speedfast1.03s to first token, ~116 tokens/sec sustained on a ~250-word technical paragraph.
Cold startfastFirst call of the session: 1.3 seconds. Ox-alpha's first call took 87.

The long-context result matters most here. I didn't stop at the advertised number — I kept pushing the haystack bigger until the API refused it. Retrieval held clean, including the honesty probe, all the way to 974,950 reported prompt tokens. The next rung up (~1,127,000 tokens) got a real HTTP 400 naming the exact overage:

"This endpoint's maximum context length is 1000000 tokens. However, you
requested about 1129425 tokens (1127425 of text input, 2000 in the output).
Please reduce the length of either one, or use the context-compression
plugin to compress your prompt automatically."

Compare that to ox-alpha: past ~660k tokens, ox-alpha returned HTTP 200, finish_reason: "stop", and zero bytes of content. A fake success. Space-bunny-alpha does the opposite at its own real ceiling — a specific error that tells you exactly what to change. Advertised limit and actual limit are the same number, and the failure past that number looks like a failure. That's the whole deal with trusting a context window.


Where it broke

1. Strict JSON schema is a no-op, not a crash

The registry lists response_format as supported. I sent a strict JSON Schema asking for a specific shape — order_id, customer.tier, an items array with sku/qty/unit_price_usd, a total_usd. Three runs. Here's run 1, verbatim and unedited:

{
  "order_id": "ORD-7712",
  "customer": {
    "name": "Mara Quill",
    "type": "enterprise"
  },
  "line_items": [
    { "sku": "SKU-A19", "quantity": 3, "unit_price": 12.50, "currency": "USD" },
    { "sku": "SKU-B22", "quantity": 1, "unit_price": 199.99, "currency": "USD" }
  ],
  "total": 237.49,
  "currency": "USD",
  "priority": "high"
}

Notice what this is not: not markdown-fenced, not an apology about a missing schema, not garbled. It's clean, valid JSON. It's just the wrong shape — customer.type instead of customer.tier, line_items/quantity/unit_price instead of items/qty/unit_price_usd, no total_usd key at all. The content comprehension is actually correct every time — right customer, right items, right quantities, right arithmetic (237.49, computed correctly all three runs) — the model just extracts it as if you'd asked in plain prose and never saw your schema. I ran this three-attempt test in all three sessions: 9 runs total, 9 different field-naming choices, zero that match the contract. One session even came back markdown-fenced on its first attempt, where this session didn't — the exact presentation varies, the failure to honor the schema never does. response_format is listed as a supported parameter on this route and functions as a no-op.

Then I checked whether that's a stealth-route problem or a MiniMax problem, by running the identical extraction against the real, named minimax-m3:cloud on Ollama Cloud — official distribution, same weights, no stealth wrapper anywhere in the path. Same result: customer.type instead of tier, quantity/unit_price instead of qty/unit_price_usd, markdown-fenced this time, plus a paragraph of unsolicited commentary ("If your target schema differs... let me know"). That settles it: this is a MiniMax-M3 trait, not something OpenRouter's stealth gateway is doing to the request. Worth the contrast with the head-to-head table below — GLM, Grok, DeepSeek, and Claude Opus all honored this exact schema cleanly on the first try. Not every model struggles with this. This one does, with or without the stealth badge.

2. Forced tool_choice is ignored

I forced tool_choice: {"type": "function", "function": {"name": "calculator"}} on the message "Just say hello to me. Do not check anything." The model's full response:

content: "Hello! 👋"
tool_calls: null

Correct social read of the room. Wrong contract. The parameter said you must call this function; nothing enforced it, and no error surfaced either — the call just quietly did the socially-sensible thing instead of the specified thing. Same result in all three sessions: 3 for 3, ignored every time.

3. The real bug isn't downtime. It's lying about the downtime.

To be precise about what's being reported here: an anonymous stealth backend occasionally being unavailable is not a bug. It's the expected cost of running on something with less redundancy than a named flagship model, and I'd say the same about any provider. The bug is what happens when it is unavailable — the gateway answers with HTTP 200, the status code that means "here is your successful response," instead of a 502 or 503 that means "something failed, try again." That's a protocol-honesty problem, not an uptime complaint.

This is the one that matters most if you're building anything on this route. A single session isn't a valid sample for a failure rate, so I ran the same 16-call core battery three separate times instead of trusting one run. Combined: 3 of 48 calls came back as HTTP 200 with a body that is not a chat completion at all — but all three landed in the same session, and the other two sessions had zero. Here's one of them:

HTTP 200 OK
{
  "id": "gen-1790951418-3fl2cX8KEseF2cHYx2eq",
  "error": {
    "message": "Provider returned an empty response",
    "code": 502,
    "metadata": { "error_type": "provider_unavailable" }
  }
}

There is no choices array. There is no content. There is a 502-flavored error nested inside a 200 status code. Any pipeline — and this covers most pipelines, including my own first pass at the test client before I caught it — that checks only the HTTP status code and reaches for response.choices[0].message.content will either throw on a missing key or, worse, catch that and move on as if the call simply returned nothing. It is not rate limiting (no Retry-After, no 429). A clean retry of the exact same call always succeeded immediately after. And the gateway isn't incapable of honest error codes — the context-ceiling test earlier in this post got a correctly-coded 400 the moment the request actually exceeded the limit. Same route, same session, two different failure modes: one reported honestly, one not.

SessionCore-battery callsDisguised-200 failures
1163
2160
3160
Combined483 (6.2%)

That changes the conclusion in a useful way: this isn't a steady ~8% background failure rate, it's bursty — something went wrong for a stretch of one session and then stopped. Three calls out of forty-eight is still three calls too many if your pipeline trusts the status code alone, but "bursty and clustered" points at a transient upstream issue (a provider blip, a capacity event) rather than a structural one-in-twelve tax on every call. Either way, the fix is the same: check for choices, not just status code.

4. The reasoning-effort dial is real, but its own billing field lies about it

The registry's "mandatory reasoning, five effort tiers" claim checks out — sometimes. On "just say hello," no reasoning trace appeared at all, mandatory or not. On a harder arithmetic prompt, the model visibly reasons — the raw reasoning field returns real, legible chain-of-thought:

"We need answer only integer. Need compute 6839*5724 accurately. Let's
calculate. 5724*(6800+39) = 5724*6800 + 5724*39. 5724*68*100: 5724*68 =
*(70-2) = 400680-11448 = 389232; *100 = 38,923,200. ..."

I ran this same effort-dial check across all three sessions rather than trusting one:

Sessionlowhighmaxreasoning_tokens (all 3 levels)
1581051710, 0, 0
2811141800, 0, 0
396881270, 0, 0
Average78102159—

Session 3 isn't strictly monotonic — high cost fewer tokens than low that run. The averages still trend the right way (78 → 102 → 159), so the dial does something real on average, but don't expect every single call to respect the ordering; this looks like normal sampling variance in how much the model decides to think, not a broken dial. What's consistent across all three sessions, with zero exceptions in nine checks: usage.completion_tokens_details.reasoning_tokens reported exactly 0 every time, including on calls where the visible reasoning text ran to several hundred characters. If you're doing capacity planning or cost attribution off that field — which is the field OpenAI-compatible tooling is built to read for exactly this purpose — you will undercount this model's actual thinking cost by the full amount every time. Budget off completion_tokens, not the reasoning-specific sub-field, on this route.

Hidden-tax footnote. Every call, regardless of content, carries a fixed ~149-token prefix reported as cached_tokens — rising to 181 the moment you attach tool definitions (the tools array itself adds 32 tokens to that fixed, cached prefix). This held constant across 25+ varied calls in this session: trivial prompts, vision calls, long-context calls. There's a fixed system preamble on every conversation before your prompt even starts, and it's big enough that a one-word request like "PONG" spends more of its prompt budget on that prefix than on your actual message.


Then I made it fight the neighbors — with today's roster, not August's

Model slugs on this gateway don't stay still for two months. Before running the head-to-head, I pulled the live model list rather than reuse names from the ox-alpha or union-alpha posts — claude-opus-5 and glm-5.3 are both gone from the catalog already, replaced by claude-opus-4.7 and glm-5. I ran the same three structural prompts, verbatim, against five models confirmed live today:

ModelStrict schemaParallel toolsForced choiceNotes
stealth/space-bunny-alpha✗ wrong shape✓✗ ignoredfree (preview, 3 days left)
z-ai/glm-5✓✓✓reasons by default (607/46/55 reasoning tokens)
x-ai/grok-4.7✓✓✓reasons by default (143/35/90 reasoning tokens)
deepseek/deepseek-v3.2-exp✓✓✓cheapest run of the five, $0.0004 total
anthropic/claude-opus-4.7✓✓✓—
anthropic/claude-fable-5.1✓✓n/a — HTTP 400this Claude route rejects forced tool_choice outright rather than ignoring it

Total spend across all five paid comparisons: about six cents. Two things fall out of this table. First, the GLM failure signature from my ox-alpha post in August is simply gone — glm-5 passes every test here cleanly. Six weeks was enough time for someone to fix that plumbing, which means failure-mode matching is a much weaker identity signal now than it was in August, and I'm not going to lean on it the way that post did. Second, every live model I could still name on this gateway today handles at least the strict-schema and parallel-tool contracts that space-bunny-alpha doesn't. That rules out "all stealth-tier routes on OpenRouter are like this" as an excuse. This is specific to this route, not a tax on anonymity in general.


So what is it? First the wrong lead, then the real one

The union-alpha post taught me not to do this: an earlier draft of it inherited the previous model's identity guess before the real battery ran, and the data contradicted it. So here's the full trail this time, including the part where I got it wrong first.

Solid regardless of identity: the hidden-preamble tax. The ~149-cached-token prefix on every call (181 with tools attached) isn't a guess. It's a flat, repeated measurement across 25+ calls this session, and it reproduced again on every re-check below.

My first identity pass, and why it was weak. I ran the same nine adversarial strings same-gateway against five named peers — Llama-3.1-8B, Llama-3.3-70B, GLM-4.7-Flash, Qwen3.5-27B, DeepSeek-V3.2-exp — and compared the residual (candidate token count minus space-bunny-alpha token count) string by string. GLM and Qwen diverged hard (std. dev. 19.0 and 41.6) because both have digit-efficient BPE vocabularies that space-bunny-alpha's tokenizer doesn't share. DeepSeek and the two Llama variants were flatter (std. dev. 4.1–5.1), which is real signal against GLM/Qwen but nowhere near proof of anything specific. I called that a 55–60%-confidence lean toward "Llama-family-shaped." That published number was honest about its own weakness, but it also wasn't the right family — because I hadn't thought to test the one lead that actually mattered yet.

Going back for a second pass. While researching this model after a first draft, I found one writeup, at cellcog.ai, covering the same question. It cited a tokenizer test from a group it calls "MarMar Labs" hitting a clean match across its own probe set, and named MiniMax's M3.1-Flash-Preview as the likely base. That's the one outside lead I had — one post, found by digging around, not a chorus of community chatter. I hadn't tested MiniMax at all in my own first pass — an obvious gap in hindsight, since MiniMax is a live, named family on the same OpenRouter gateway I was already probing everything else on. So instead of taking cellcog.ai's word for it, I went and ran it myself: same methodology, same nine probes, same-gateway, against every MiniMax model OpenRouter actually lists — minimax-m3, minimax-m2.7, minimax-m1, and minimax-01. (MiniMax's "M3.1-Flash-Preview" is not a live slug on OpenRouter's catalog as of this session, so I could not test that exact point release directly through a chat call — more on what that does and doesn't mean below.)

CandidateResidual vs space-bunny-alphaStd. dev. (9 probes)Reproduced?
minimax/minimax-m1exactly +290, every probe0.00yes — identical on independent re-run
minimax/minimax-01exactly +555, every probe0.00yes — identical on independent re-run
minimax/minimax-m2.7−113 to −1150.83close but not exact — one probe shifted by 2 tokens on re-run
minimax/minimax-m3 (OpenRouter-served)+7 to +20 (bimodal)6.13noisy at this endpoint — resolved below with the raw tokenizer file

minimax-m1 and minimax-01 already land on a perfectly flat offset across nine structurally different strings, which doesn't happen between unrelated vocabularies by chance. But minimax-m3 — the one actually named after the generation cellcog.ai's source pointed at — came back noisy at the API layer (std. dev. 6.13), and I wasn't going to round that up to a match just because it was the closest-sounding name. So I went one level deeper than any API usage field: I pulled MiniMaxAI/MiniMax-M3's real, published tokenizer.json straight from Hugging Face and ran the same nine probes through it directly, no chat endpoint, no routing wrapper, nothing but raw tokenizer.encode() calls — the identical method I'd already used for the Llama/Mistral/Qwen local-file comparisons earlier in this post.

Once that worked, I didn't stop at one checkpoint. I pulled the published tokenizer.json for every MiniMax generation on Hugging Face with one — M1, M2, M2.1, M2.5, M2.7, and M3 — and ran the same raw test against all six. The files themselves aren't identical: M1 has its own file hash, M3 has its own, and all four M2.x releases in between share one identical file. But every single one of the six, hash differences and all, produces the exact same flat −156 offset against space-bunny-alpha, stdev 0.0000. MiniMax has kept the same effective vocabulary, at least for everything these nine probes touch, across its entire public lineage.

ProbeMiniMax-M3 raw tokenizerspace-bunny-alpha (usage)Delta
en_pangram10166−156
emoji_zwj21177−156
zh_long20176−156
fr17173−156
ru19175−156
ja10166−156
digits67223−156
code30186−156
repeat_eq3159−156

Every probe, the same delta: exactly −156. Standard deviation 0.00. Pairwise cross-check (comparing every probe against every other probe, which cancels any constant wrapper and tests the raw shape of the vocabulary itself): 36 of 36 exact matches. That's not a lean and it's not a hint — a 36-for-36 pairwise hit across nine content types this different, from a vocabulary pulled straight from the model's own public weights, is the same bar I set for a fingerprint "hit" back in the ox-alpha post, and this clears it completely. The earlier noise at the OpenRouter-served minimax-m3 endpoint turns out to be exactly that — noise from that endpoint's own serving-layer wrapper, not the underlying vocabulary. The raw tokenizer file was the right test all along; the API usage field was a noisier proxy for it.

That resolves the tokenizer half of what cellcog.ai's writeup claimed — the "MarMar Labs" 36-for-36 match against MiniMax's M3 tokenizer, which I just reproduced myself with a primary-source test instead of taking it on their say-so. The other half of their case was a spec comparison naming M3.1-Flash-Preview specifically, and I checked that independently too rather than repeating it on their word: MiniMax's own M3.1-Flash-Preview, announced September 27, 2026, carries a 1,000,000-token context window and the identical five-tier reasoning-effort ladder — low, medium, high, xhigh, max — that space-bunny-alpha's OpenRouter listing advertises, confirmed across three independent trade write-ups of that announcement. Public reporting also notes M3.1-Flash-Preview normally runs only inside MiniMax's own coding tool rather than through a public API slug — which is exactly why I couldn't find or test that exact checkpoint name on OpenRouter directly, and fits neatly with "an anonymous stealth route is how you quietly put a gated model in front of outside traffic."

The tokenizer test isn't the only cross-check available. Since the lead is specifically MiniMax, I ran the same reasoning battery and vision test against the real, named minimax-m3:cloud on Ollama Cloud — official distribution, no stealth badge anywhere.

Teststealth/space-bunny-alphaminimax-m3:cloud (named)
Reasoning battery4/44/4
Vision (same test image)5/55/5
Parallel tool callsCleanClean
Strict JSON schemaBroken — wrong field namesBroken — same wrong field names, fenced
Forced tool_choiceIgnoredUntestable — Ollama's API has no forcing mechanism

Every behavior that's actually comparable matches, including the specific way the schema test fails. That's not proof by itself — lots of models get 4/4 on basic arithmetic — but it's one more independent line pointing the same direction as the tokenizer test, run through a completely different path (Ollama Cloud, not OpenRouter; the model's own name, not a stealth badge).

So: this is MiniMax. That part I'll put a number on — call it 97%, on the strength of a reproduced 36/36 primary-source match. Specifically M3.1-Flash-Preview is a weaker claim than that, and it deserves its own number, not the same one borrowed from the tokenizer test.

Here's why those two claims aren't equally certain. The tokenizer match proves the family, not the specific checkpoint — and now I have the data to show exactly how little it discriminates within that family. Six separate published MiniMax tokenizers, spanning M1 through M3, all tokenize these nine probes identically to space-bunny-alpha. That's strong evidence it's MiniMax. It is zero evidence for which MiniMax: the test can't tell M3.1-Flash-Preview apart from M1, M2, or anything else sharing that vocabulary, because none of them differ on the thing I'm measuring. It rules out Llama, GLM, Qwen, and DeepSeek completely; it rules out nothing inside MiniMax's own lineage. What points at M3.1-Flash-Preview specifically is a separate, weaker line of evidence: three spec matches (context size, mandatory reasoning, the same five-tier effort ladder) that aren't generic across this gateway, plus a reported reason the real checkpoint wouldn't be independently callable to test. That's a reasonable case, not a proof. I'd put it at 70–75% — good enough to run with, nowhere near the confidence behind "it's MiniMax."


Free is a launch price

The last two posts in this series had to argue that "free" was temporary by inference — a pricing row that read $0.00 with an expiration date decades out. This one doesn't need the argument. expiration_date: 2026-10-05 is sitting right in the registry response, three days from today. Whatever this is testing, the test has an end date, right in the API response.

That changes the advice slightly from the last two posts. It's not "architect so the free price isn't load-bearing." It's "assume this stops working on October 5, because the registry is telling you exactly when." The engine underneath — the 975k-token honest retrieval, the clean parallel tool calls, the fast cold start — may resurface under a different name, a different price, or not at all. Not every envelope problem is gateway plumbing, either: the schema failure reproduced identically on the real, named minimax-m3:cloud, so that one travels with the weights and won't get patched away by OpenRouter. The forced-tool_choice ignore and the disguised 502s are still plausibly route-specific — I couldn't test forced tool_choice against the named model at all, since Ollama's API doesn't expose that mechanism, and the 502-in-200 behavior is specific to this gateway's own error handling by definition. Don't bet on any of it in either direction. Bet on what you measured.


If you're running it today

  1. Never trust response_format with this model, on any route. This one reproduces on the real, named MiniMax-M3, not just the stealth wrapper. Ask for JSON in the prompt by name, parse it yourself, validate the shape, reject loudly on a mismatch — the content is usually right, the field names won't be.
  2. Treat tool_choice as a strong hint, not an enforcement mechanism. Check that the forced tool was actually called before trusting that it was.
  3. Check for a nested error key even on HTTP 200. This is the one most pipelines get wrong by default: status-code-only error handling will silently swallow a fake success with null content. Measured rate across three sessions: 3/48 (6.2%), clustered in one session, not steady. Check for choices presence, not just status code, regardless of how rare it looks on any given day.
  4. Cost and capacity-plan off raw completion_tokens, not completion_tokens_details.reasoning_tokens. The latter reports zero regardless of how much real, visible thinking happened.
  5. The context window checks out, up to a point. Clean, honest retrieval through 974,950 reported tokens, and a real 400 past the 1,000,000 ceiling. Budget under a million. This is the first of the three stealth models in this series where the advertised ceiling and the measured ceiling are the same number.
  6. It's gone in three days. Don't build anything into this preview window that you can't unplug by October 5 without a scramble.

The whole thing in five lines

# the context claim is the most honest of the three stealth models so far
974,950 tokens clean, honest NONE on the phantom needle, then a real 400 at 1,000,000

# the envelope still drops two contracts
response_format -> silently reshaped; confirmed on named minimax-m3:cloud too, so it's the model, not the route
tool_choice     -> ignored, not enforced

# the gateway lies about its own health, 3/48 across 3 sessions, bursty not steady
HTTP 200 + {"error": {"code":502, ...}} looks like success to anyone checking status codes only

# the lineage, with numbers instead of "almost certainly"
this is MiniMax: 97% -- reproduced 36/36 tokenizer match against MiniMax-M3's real weights
specifically M3.1-Flash-Preview: 70-75% -- 3 spec matches, not a tokenizer file for that checkpoint
first pass guessed Llama and was wrong; cellcog.ai's MiniMax call was right

# the clock is the headline
expires 2026-10-05 -- don't guess, inspect, and don't build what you can't unplug in 3 days

Related reading


Keith is a database consultant and infrastructure engineer with 25+ years of open-source experience. He builds the ApocryiaAI brand and DRAVEN work under 3k1o LLC (apocryia.com and related domains). He writes about MySQL, Proxmox, AI memory, multi-agent workflows, and building technology you can actually inspect. Test transcripts from this post are on disk; ask and I'll show you the raw pairs.