Showing posts with label deepseek. Show all posts
Showing posts with label deepseek. Show all posts

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.

Saturday, August 22, 2026

OpenRouter Handed Me a Free Mystery Model. I Put It Through a Real Scorecard.

OpenRouter Handed Me a Free Mystery Model. I Put It Through a Real Scorecard.

I've got eleven Claude Code sessions running on a stealth model called ox-alpha that costs exactly nothing — for now. Twenty-four hours in, my honest read: not perfect, but good. Instead of trusting that, I pointed a scripted test battery at it and kept the transcripts. The engine is real: 600,000-token needle retrieval, honest answers when the answer is nothing, working vision. The envelope around the engine is broken in three ways that matter to anyone doing agentic work. And three independent fingerprints all point in the same direction: Zhipu.


Liking it isn't the same as trusting it

To be clear up front: I am not here to tell you the model is bad. Twenty-four hours in, my honest verdict is the one above — not perfect, but genuinely good. Is it at Fable or Opus or Grok level yet? No — and unlike most "is it frontier yet" arguments, I can put numbers under that answer instead of adjectives. On the exact same three structural prompts, Opus-5 answered in 4.8s, 3.2s, and 2.6s, and Fable in 5.6s, 2.7s, and 1.9s — both honoring every contract. Ox-alpha took 69 seconds, 20 seconds, and 12 seconds on those identical prompts — and got all three wrong. Nothing in a day of real work says the top seats change hands; the Critic keeps its Anthropic badge for now. But free came closer to those seats than free has any right to, and that gap is worth measuring precisely. The question this post answers is narrower: which parts of it are good enough to depend on, and which parts will bite you on a Tuesday. Liking a tool earns it a place in your rotation. Trusting one earns it a place in your architecture. Those are different thresholds, and only one of them comes with receipts.

The model is called Ox Alpha. On OpenRouter it lives at stealth/ox-alpha, it went live on August 20, and it is priced at zero. Not "free tier with limits" — the pricing fields on the API are literally "0" and "0". A third-party provider built it, chose to stay anonymous during the preview, and OpenRouter just routes to it.

I moved a pile of real work onto it. Eleven concurrent Claude Code sessions on the last ps snapshot, running through OpenRouter, doing the kind of long-horizon coding and ops work I'd normally spread across models like Kimi, Opus, Grok, and Ollama Cloud's GLM shelf. I've been testing it for about 24 hours now. Do I like what I see? Yes. Is it 100% perfect? No. But it is good — and "good but unproven" is exactly the state where discipline matters, because good-enough-to-depend-on is a claim, and claims need evidence.

That's the trap with any new model, free or not: a good first day is an anecdote wearing a lab coat. It's the same trap as reading a vendor's spec sheet and assuming the parts you care about work the way the table says — advertised capability is not capability, and the only cure is a probe against the live endpoint.

So I built a probe. A proper one.

Don't guess — inspect.
Especially when the thing you're inspecting is free, anonymous, and eating your workweek.

The envelope, verified

First, what does the API itself claim? Pulled live from OpenRouter's model registry:

ClaimValue from the registry
Context window1,048,576 tokens
Max output131,072 tokens
Modalitiestext+image+video → text
Tool calling / structured outputboth listed as supported
Reasoningmandatory — always on, effort dial only goes low / high / max. There is no off switch.
Pricing$0 input, $0 output
Tokenizerlisted as "Other" — no vendor attribution
Upstreamreported simply as "Stealth" on every response

Two things in that table deserve a second look. The mandatory reasoning means even "reply with the word PONG" burns thinking budget on every call — more on the cost of that below. And the anonymous tokenizer is the thread I'll pull later, because it turns out to be the loudest clue about who built this thing.


How I tested — and who graded it

A confession that cuts both ways: the agent that wrote and ran this battery runs on ox-alpha itself. I used the model to grade the model.

I'm not pretending that's ideal. I am saying it's manageable, because the battery was built so that nothing rides on judgment. Every test has a ground truth computed by the script — the expected product of two integers, the expected letter count in a sentence the script generated, the expected vault codes hidden in a log the script assembled. The grader is regex and arithmetic, not vibes. If ox-alpha wanted to flatter itself, it would have to alter arithmetic.

The battery:

  • Objective reasoning — multi-digit multiplication, a chained word problem with a non-integer midpoint, case-insensitive letter counting, and a bat-and-ball-style trap with changed numbers so a memorized answer fails.
  • Structured output — strict JSON Schema extraction, three runs.
  • Tool calling — single call, parallel calls across cities plus math, and a forced tool_choice.
  • Long context — synthetic logs with unique audit needles planted at 10%, 50%, and 90% depth, plus a fourth needle that was never planted. Asking about the phantom one is the honesty probe: a model that fills from vibes will report a color for something that doesn't exist.
  • Vision — a rendered image with known text at two sizes and four shapes whose colors and positions the script knows.
  • Latency — time-to-first-byte and tokens/sec on streamed calls.

What held up

TestResultDetail
Reasoning battery4/4847×396, the decimal chain (86.95 exactly), letter count, the changed-numbers trap. All clean.
Long-context retrievalclean through 601kAll three needles found at 120k / 200k / 400k / 601,687 reported prompt tokens.
Honesty at depthperfectThe never-planted needle came back NONE at every depth. No confabulation anywhere in the haystack.
Vision5/5Read the large code, read the small-print serial number, placed all four shapes correctly — including noting the triangle points downward.
Effort dialrealSame multiplication task: 56 completion tokens at low effort, 97 at high, 322 at max. Correct all three times.
Streaming speedusable~6.7s to first token warm, ~33 tokens/sec sustained.

The long-context result is the headline. This is not "the API accepted a big prompt." The needles were buried in filler text at specific depths, and retrieval was perfect from shallow to 600k-plus — including the part most models fail, which is admitting a needle isn't there. Whatever is under the anonymity badge, somebody taught it to distinguish knowing from guessing. I have opinions about why that might be. They come later.


Where it broke

Three structural failures, all on the axes the marketing copy leads with — coding, agents, production workloads. Each one reproduced cleanly.

1. Strict JSON schema is theater through this route

The registry advertises structured output. I sent a strict JSON Schema asking for a fixed shape — order ID, customer tier, items with quantities, total. Three runs. Zero parseable responses. Here's the actual opening of the reply, unedited:

Since no specific schema was provided, here's the order extracted
into a standard JSON structure:

```json
{
  "order_id": "ORD-7712",
  ...invented field names from here...

The model never saw my schema. The parameter is listed as supported; somewhere between OpenRouter and the anonymous upstream, it silently evaporates — and the model politely announces that no schema arrived. Your validation layer gets markdown fences and invented field names instead of the contract you asked for.

2. Parallel tool calls collapse into illegal JSON

I asked for two weather lookups and a calculation. This is the bread and butter of an agent loop. The model emitted two tool calls — but the weather call's arguments looked like this, verbatim from the transcript:

{"city":"Tokyo","city":"Paris"}

Two identical keys in one object. That is not valid JSON in any meaningful sense — the spec explicitly says duplicate names are ambiguous. What happens next depends on which parser your stack uses: Python keeps Paris, some C++ parsers keep Tokyo, strict validators reject the whole call. One request, three different outcomes, none of them flagged as an error.

This is the nastiest class of bug there is: not a crash, but silent environment-dependent data loss inside the exact workflow the model is sold for. A missing tool call throws. A mangled one quietly does the wrong job.

3. Forced tool_choice is ignored

The API supports forcing a specific tool. I forced calculator on a message that said "just say hello." The model replied with a greeting. Its hidden reasoning trace, which the route does expose, said it all:

"The user just wants a simple greeting. No tools needed."

Correct social instinct. Wrong contract. The parameter said call the calculator; nobody enforced it.

4. Past ~650k tokens, it fails like a liar, not like a server

The advertised context is 1,048,576 tokens. Retrieval stayed perfect at 601,687. Then I pushed the haystack bigger, and at roughly 660,000 tokens something interesting happened:

HTTP 200 OK
finish_reason: "stop"
content: ""          <- a="" back="" bytes="" code="" count="" even="" not="" null="" token="" usage:="" zero="">

No error code. No truncation notice. A success-shaped response containing nothing. If your pipeline checks status codes — and most do — it will treat silence as success and propagate emptiness downstream. The honest behaviors here are a 400, or a truncated flag, or at minimum a usage object telling you how much of your prompt survived. I got a smile and shrug.

Latency footnote. Cold start is rough: the very first call took 87 seconds to return the literal word PONG. Warm calls run 3–40 seconds depending on how much mandatory thinking the router applies. And because reasoning cannot be switched off, a trivial extraction that should cost ~80 tokens burned 500–700 on hidden thinking. Budget accordingly.

Then I made it fight the neighbors

A scorecard against itself is incomplete. So I replayed the three structural tests verbatim against seven named competitors on the same aggregator, same prompts, same day. Total spend for the entire comparison matrix: about eleven cents.

ModelStrict schemaParallel toolsForced choiceList price (in/out per M)
stealth/ox-alpha✗ 0/3 parseable✗ duplicate keys✗ ignoredfree (preview)
z-ai/glm-5.3✗ same failure✓✗ ignored$1.40 / $4.40
~z-ai/glm-latest✗ empty reply✓✗ ignored$1.40 / $4.40
x-ai/grok-4.6✓✓✓$2.00 / $6.00
~deepseek/deepseek-v4-flash-latest✓✓✓$0.065 / $0.18
anthropic/claude-opus-5✓✓✓$5.00 / $25.00
anthropic/claude-opus-5-fast✓✓✓$10.00 / $50.00
~anthropic/claude-fable-latest✓✓✓$10.00 / $50.00

Read that table twice, because it kills two lazy takes at once.

First take killed: "that's just OpenRouter mangling parameters." No. Five other providers' routes honored the exact same parameters on the exact same day. Only the stealth route and the Zhipu routes dropped them.

Second take killed: "free wins because free." DeepSeek-V4-Flash passed every single test ox-alpha failed, at $0.065 per million input tokens. The cheapest model in the comparison is also the cheapest way to avoid engineering around ox-alpha's holes. Free isn't a price once you pay in workarounds.


So what is it? Three fingerprints say Zhipu

The community has been guessing since launch day, mostly landing on a Zhipu / Z.ai GLM-family model. I wasn't going to settle identity by vibes, so I ran three independent forensic probes.

Fingerprint one: the tokenizer matches GLM to the token. I sent six adversarial strings — plain English, mixed Chinese-English, snake_case code, 300 repeated delimiters, mathematical-alphabet Unicode with emoji, a 200-digit run — through ox-alpha and through candidate families, then compared token counts string by string. Against GLM-5.2, the difference was exactly 75 tokens on every single string: prose, CJK, delimiters, digits, fraktur, all of it. A constant offset means template overhead; identical per-string counts mean the same tokenizer making the same decisions on every stress case. Meanwhile DeepSeek drifted to −142 on digit runs and Kimi to −138, and Gemma disagreed wildly everywhere. Six strings, zero divergence, only against GLM. That's not coincidence; that's lineage.

(Bonus leak: the constant 75-token offset means the stealth route injects its own hidden system preamble into every conversation. Something is whispering instructions to your model before you speak.)

Fingerprint two: identical failure handwriting. Remember the schema-test confession — "Since no specific schema was provided, here's the order extracted into a standard JSON structure." GLM-5.3, routed separately and billed normally, opened its own failed attempt with: "Since no schema was attached, here's the order extracted into a standard JSON structure." Same sentence shape, same fence, same invented field renames. Two different accounts, two different routes, one shared serving stack behaving identically badly.

Fingerprint three: only the Zhipu family ignores forced tools. Across the whole matrix, exactly three entries blew off tool_choice — the stealth model and the two Zhipu entries. Grok, DeepSeek, and both Claude seats obeyed the contract.

Token counting, failure mode, and parameter handling all pointing the same direction puts me near 85% confidence on a GLM-family upstream — up from the 65–75% I'd have guessed from capability vibes alone. One honest wrinkle keeps me off 95%: ox-alpha mangled the parallel-tool test in a way retail GLM-5.3 did not, so whatever this is, it isn't byte-for-byte yesterday's checkpoint. Different build, same bloodline.


Free is a launch price

Say the quiet part out loud: this is free the way a dealer's first sample is free.

Every indicator says limited-time preview: a $0.00 pricing row, an expiration date decades out in the registry, an anonymous provider buying market data with compute they already have. None of that is a complaint — it's a rational launch strategy, and smart people are getting real work done on their dime. But build your architecture around the current envelope and read the fine print later, and the bill arrives all at once: the day the preview ends, pricing appears, and whatever the free period taught your pipelines to tolerate becomes your new dependency surface.

So the question is not "is it good?" It's "what survives the meter starting?" The engine will still be the engine when it costs money — 600k-token retrieval doesn't evaporate with a pricing change. The envelope may improve overnight too, because these particular failures smell like plumbing, not weights: the schema and tool-choice drops look like translation gaps between OpenRouter and the upstream, and plumbing can be fixed without touching a checkpoint. But you don't get to count on that. What you get to do is architect so that neither the free price nor the broken envelope is load-bearing in anything you keep.


If you're running it today

The rules I'm applying to my own fleet, effective immediately:

  1. Never trust response_format on this route. Ask for JSON in the prompt, strip fences, validate against the schema yourself, reject loudly on violation.
  2. Put a duplicate-key detector on tool dispatch. Parse arguments with a hook that flags repeated keys, and split them into separate calls before execution.
  3. Treat empty completions as retryable failures. Check finish_reason AND non-empty content AND a usage object. Any one missing: back off and retry, never pass silence downstream.
  4. Cap prompts around 600k tokens despite the advertised million. The cliff sits somewhere past 600k and it's shaped like a success.
  5. Validate one call end-to-end before any batch. Same rule I apply to every paid API; free endpoints deserve it even more, because nobody reads errors off a free meter.

The whole thing in five lines

# the engine is real
600k-token needle retrieval, perfect, and honest when the needle isn't there

# the envelope is not
json_schema -> silently dropped   parallel calls -> duplicate keys
forced choice -> ignored          ~660k prompt -> HTTP 200, zero bytes

# the lineage leaves prints
tokenizer, failure handwriting, and parameter handling all say Zhipu

# the price is temporary
free is how you buy market share; architecture like the meter is running

# the discipline that found all of this
don't guess -- inspect

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.