Add OpenRouter as a gateway for any model - #31
Conversation
OpenRouter joins DeepSeek and MiniMax as a gateway provider: the stock claude binary runs against https://openrouter.ai/api with the operator's OPENROUTER_API_KEY, an isolated config dir, and the empty ANTHROPIC_API_KEY that OpenRouter's Claude Code guide requires. Only OpenRouter gets the empty key, so DeepSeek and MiniMax env is unchanged. There is no model allowlist. The flex matrix carries one open openrouter row; each distinct OpenRouter model ID is its own family, and setup's live probe on the named model is the gate. The OpenRouter probe reads its marker from a file, so a pass proves a tool call. The runner refuses an ID without a namespace and OpenRouter's own openrouter/* routers, which pick the model server-side. OpenRouter reports must match the requested ID exactly apart from case, since a prefix rule would accept a sibling model. Panel diversity counts the lab behind the model: an OpenRouter lane counts as its ID's namespace. Refs #7
LANES.md gains an "OpenRouter: any model, one key" section, its env variables, keychain lines, price note, and a V6 route battery, and drops the "not shipped" note. README, USAGE, and LIVE-GATE name the new provider. UPSTREAM-FLEX lists the OpenRouter surfaces as fork-owned. docs/plans/2026-10-05-openrouter-gateway.md holds the plan with its runtime and setup flow diagrams. Refs #7
An OpenRouter dry run with an invalid key showed two gaps on Claude Code 2.1.289. A rejected key comes back as "Failed to authenticate. API Error: 401" with "api_error_status":401, which the unavailable-status pattern missed, so the lane was child-failed instead of unauthenticated. And usage.output_tokens_details.thinking_tokens was dropped; it is now recorded as reasoningTokens. Both apply to every claude-binary lane. Refs #7
scripts/probe-openrouter.sh runs issue #7's checks through pstack-runner: a two-file tool chain whose value is not in the prompt, an effort comparison by reasoning tokens, OpenRouter's own view of the served model and host, and the failure strings for a wrong ID, a bad key, a router, a rolling alias, a :free variant, and a model without tools. It spends real credit, so it stays out of check.sh and CI. The key is read from the environment and never reaches an argument list. docs/gateway-model-probes.md records the invalid-key dry run: a 401 that Claude Code retries for about three minutes. LANES.md notes the slow failure and points V6 at the script. Refs #7
How to testThis PR stays a draft until both parts below pass. Record the results in the evidence block at the end. 1. Route probes (script, real key)Store the key once, in your own terminal (it prompts for the value): security add-generic-password -a "$USER" -s pstack-openrouter -wOn OpenRouter's site, add a few dollars of credit, set a credit limit on the key, and turn off data collection in the privacy settings. Run the battery from the branch checkout. It takes about 10 minutes and costs a few cents: OPENROUTER_API_KEY=$(security find-generic-password -a "$USER" -s pstack-openrouter -w) \
bash scripts/probe-openrouter.shCommit the printed tables to 2. Live gate (both apps)Install this branch in place of any older # Claude Code
claude plugin uninstall pstack@open-pstack
claude plugin marketplace remove open-pstack
git clone -b feat/openrouter-gateway https://github.com/thisguymartin/pstack-flex ~/src/pstack-flex-candidate
claude plugin marketplace add ~/src/pstack-flex-candidate
claude plugin install pstack@pstack-flex
# Codex
codex plugin remove pstack@open-pstack
codex plugin marketplace remove open-pstack
codex plugin marketplace add thisguymartin/pstack-flex --ref feat/openrouter-gateway
codex plugin add pstack@pstack-flexStart each app with the key loaded, so OpenRouter lanes can see it: export OPENROUTER_API_KEY=$(security find-generic-password -a "$USER" -s pstack-openrouter -w)
claude # and `codex` in a second terminal, same export firstIn a new session in each app:
EvidenceOne block per app, in the PR description under "Live evidence" (format from |
Closes #7
What changed
OpenRouter becomes a third gateway provider, next to DeepSeek and MiniMax. No new harness: the lane is the stock
claude -pbinary pointed athttps://openrouter.ai/apiwithOPENROUTER_API_KEY, an isolated config dir, and the emptyANTHROPIC_API_KEYthat OpenRouter's Claude Code guide requires.openrouterrow. Any OpenRouter model ID works for any role, such asopenrouter:moonshotai/kimi-k3@high. Each distinct ID is its own family with its own effort and probe, and setup's live probe on the named model is the gate. The OpenRouter probe reads its marker from a file, so a pass proves a tool call.openrouter/*routers (auto,free), which pick the model server-side. Every model a router could pick is reachable by its own ID.z-ai/glm-5.3-airforz-ai/glm-5.3.anthropic,openai,x-ai,deepseek, andminimaxmatch the direct providers.The plan, with the runtime and setup flow diagrams, is in
docs/plans/2026-10-05-openrouter-gateway.md.Two runner fixes came out of an invalid-key dry run, and they apply to every claude-binary lane. Claude Code 2.1.289's 401 result ("Failed to authenticate",
"api_error_status":401) now classifies asunauthenticatedinstead ofchild-failed.usage.output_tokens_details.thinking_tokensis now recorded asreasoningTokens. Evidence is indocs/gateway-model-probes.md.scripts/probe-openrouter.shruns the #7 route battery. It spends real credit, so it stays out ofcheck.shand CI.Review order:
runner/flex-providers.tsrun.ts:validateOptionsandunavailableStatusparse-output.ts:reportedModelMatchesandnormalizedUsagereferences/provider-dispatch.mdandsetup-pstack/SKILL.mdStill to do before this leaves draft
bash scripts/probe-openrouter.shwith a real key, on five models from different labs (V6 indocs/LANES.md). This also captures OpenRouter's real error strings sounavailableStatuscan classify "no endpoints found" asunavailable-model(today it would bechild-failed).docs/LIVE-GATE.mdin Claude Code and Codex, with an OpenRouter model typed into setup.Verification
bash scripts/check.shreports all checks passed atb258622. One test is timing-sensitive under load:runLane > spends one explicit deadline across preflight and model executionfailed 5 of 5 runs on unchangedmainon this machine in an earlier run. It does not touch the gateway path.Live evidence:
Pending.
A pull request without live evidence remains a draft. Do not merge, tag, release, or roll it out.