Result: a real deliberation traversed a local CLI proxy and the client's own trace attributed the stage to provider=cliproxy with a non-empty wire model and a loopback apibase; the negative control (proxy stopped) failed with a connection error and tokensinput=0. No adapter code was written — only configuration and docs. Verified on chimera 0.2.6 (Python 3.12) against the reference claude-code-router 1.0.73 loopback contract on
claude-code-router / one-api / LiteLLM / vLLM) into an OpenAI-compatible client as a first-class provider — zero adapter codeResult: a real deliberation traversed a local CLI proxy and the client's own trace attributed the stage to provider=cliproxy with a non-empty wire model and a loopback api_base; the negative control (proxy stopped) failed with a connection error and tokens_input=0. No adapter code was written — only configuration and docs. Verified on chimera 0.2.6 (Python 3.12) against the reference claude-code-router 1.0.73 loopback contract on <ip-address>:3456, matching the work commits 162ce39 + docs 261daa5.
The client already had every shape needed to route to an arbitrary OpenAI-compatible endpoint; the failure was not a missing code path, it was misconfiguration plus three non-obvious behaviours. Each is a distinct root cause:
GET /v1/models does not exist, so discovery-based wiring failsclaude-code-router 1.x answers:
$ curl -s http://<ip-address>:3456/v1/models
{"message":"Route GET:/v1/models not found","error":"Not Found","statusCode":404}
Any integration that auto-populates its catalog from a model-list endpoint sees a 404 and never learns the served id. Served ids must be taken from the proxy's own config/UI.
The client (resolve_litellm_model) strips exactly one leading <provider>/ segment from the catalog id and sends the remainder verbatim as the wire model. So:
| Catalog id | Wire model sent | Proxy response |
|---|---|---|
cliproxy/deepseek,deepseek-v4-flash |
deepseek,deepseek-v4-flash |
HTTP 200 |
cliproxy/deepseek-v4-flash |
deepseek-v4-flash |
HTTP 404 Provider 'deepseek-v4-flash' not found |
For ccr, the id it accepts is its Router target written provider,model. This is the single most common wiring failure.
A loopback base_url with no api_key/api_key_env is a supported keyless shape (_is_local_base_url → provider_api_key_env returns None). But the generic branch pins custom_llm_provider="openai", and LiteLLM's OpenAI SDK path refuses to start a call with no credential at all:
OpenAIException - Missing credentials. Please pass an `api_key` ... or set the `OPENAI_API_KEY` environment variable.
Symptom is a credential error, not a connection error. Fix: OPENAI_API_KEY=local for the run (the proxy ignores it).
With the default auto_formation.restrict_to_credentialed_providers: true, provider_credential_resolved(cfg,"cliproxy") is False, so an auto formation will not assign it. Either pin the stage (--stage-models '{"worker_1":"cliproxy/<id>"}' / a formation's worker_models) or set the restriction to false.
claude-code-router 3.x is not keyless-capable3.x moves config into ~/.claude-code-router/config.sqlite and its launcher hardcodes AUTH_ENABLED/AUTH_MODE=static_api_key/AUTH_REQUIRED=true for the gateway, so a keyless call answers Missing auth token header: authorization. Use api_key_env there (and for one-api/new-api).
Measured on 1.0.73: with "LOG": true, every forwarded request is logged with headers intact — "msg":"final request" records headers.authorization: "Bearer <upstream key>" in clear text under ~/.claude-code-router/logs/ccr-<date>.log, and on the measured host those files were group/other-readable (0664). The fix commit removed "LOG": true from the recipe and ships "LOG": false.
ccr writes SQLite/log files next to its working directory. Running it inside the target checkout leaves untracked noise (same class as an MCP server dropping a .db at the repo root). Run it and install it outside the repo.
mkdir -p /tmp/ccr-lab && cd /tmp/ccr-lab
npm init -y && npm i @musistudio/claude-code-router@1.0.73 # 1.0.73 = last 1.x
~/.claude-code-router/config.json (1.x schema; Router entries are provider,model):
{
"LOG": false,
"HOST": "<ip-address>",
"PORT": 3456,
"Providers": [
{
"name": "deepseek",
"api_base_url": "https://api.deepseek.com/v1/chat/completions",
"api_key": "<the upstream provider key>",
"models": ["deepseek-v4-flash"]
}
],
"Router": {
"default": "deepseek,deepseek-v4-flash",
"background": "deepseek,deepseek-v4-flash",
"think": "deepseek,deepseek-v4-flash",
"longContext": "deepseek,deepseek-v4-flash"
}
}
️ Never ship
"LOG": truewhen the proxy fronts a paid upstream — it records the upstreamauthorization: Bearer <key>header in cleartext. If you must enable it for diagnosis,chmod 700 ~/.claude-code-router/logs(files600), keep it out of backups/syncs, and rotate after debugging.
cd /tmp/ccr-lab
node node_modules/@musistudio/claude-code-router/dist/cli.js start
# 🚀 LLMs API server listening on http://<ip-address>:3456
In chimera.yaml (mirrors chimera.yaml.example):
providers:
# Generic OpenAI-compatible base_url branch; no LiteLLM-native prefix.
cliproxy:
base_url: http://<ip-address>:3456/v1
# api_key_env: CLIPROXY_API_KEY # only for a token-enforcing proxy (ccr >= 3)
models:
# The catalog id carries the id the PROXY answers to after one `cliproxy/`.
# For ccr that is its router target, `provider,model` — NOT the bare upstream id.
cliproxy/deepseek,deepseek-v4-flash:
categories:
technology_code/code_generation/python: 84
general_knowledge/reasoning/explanation: 76
# ... scores of the UPSTREAM model the proxy fronts, not the proxy itself
cost_tier: budget
provider: cliproxy
enabled: true
Why this works with zero adapter code: cliproxy is not a native LiteLLM prefix, so the generic branch in resolve_litellm_model passes the configured base_url as api_base, pins custom_llm_provider="openai", and strips exactly one leading cliproxy/, sending deepseek,deepseek-v4-flash verbatim.
bash
OPENAI_API_KEY=local chimera ...yaml
cliproxy:
base_url: http://<ip-address>:3456/v1
api_key_env: CLIPROXY_API_KEYPin the stage explicitly:
chimera -f simple -c /tmp/cliproxy-chimera.yaml --json \
--stage-models '{"worker_1":"cliproxy/deepseek,deepseek-v4-flash",
"worker_2":"deepseek/deepseek-v4-flash",
"aggregator":"deepseek/deepseek-v4-flash"}' \
"In two sentences, explain why a local CLI proxy in front of LLM providers is useful."
or allow keyless providers into auto selection:
auto_formation:
restrict_to_credentialed_providers: false
Add docs/CLI_PROXY.md (contract table, base_url/key wiring, catalog-id → wire-model table, worked ccr example, class alternates, measured limitations, LOG warning) and link it from docs/README.md so tests/test_docs_index.py stays green.
All commands below were run on this host. The client is chimera 0.2.6 (.venv/bin/chimera); the proxy is the reference ccr 1.0.73 loopback contract on <ip-address>:3456.
# correct id -> 200, OpenAI-shaped
curl -s -X POST http://<ip-address>:3456/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"deepseek,deepseek-v4-flash","messages":[{"role":"user","content":"Reply with the single word: PROXY-OK"}],"max_tokens":300}'
HTTP 200
{"id":"46148238-...","object":"chat.completion","model":"deepseek-flash",
"choices":[{"index":0,"message":{"role":"assistant","content":"PROXY-OK",...},"finish_reason":"stop"}],
"usage":{"prompt_tokens":40,"completion_tokens":34,"total_tokens":74}, ...}
# bare upstream id -> 404
$ curl -s -X POST ... -d '{"model":"deepseek-v4-flash", ...}'
{"error":{"message":"Provider 'deepseek-v4-flash' not found ..."}} HTTP 404
# no model-list endpoint
$ curl -s http://<ip-address>:3456/v1/models
{"message":"Route GET:/v1/models not found","error":"Not Found","statusCode":404}
Command (config copied out of the repo first):
OPENAI_API_KEY=local .venv/bin/chimera -f simple -c /tmp/cliproxy-lab/chimera.yaml --json \
--stage-models '{"worker_1":"cliproxy/deepseek,deepseek-v4-flash","worker_2":"cliproxy/deepseek,deepseek-v4-flash","aggregator":"cliproxy/deepseek,deepseek-v4-flash"}' \
"In two sentences, explain why a local CLI proxy in front of LLM providers is useful."
Measured trace:
stage=worker_1 model=cliproxy/deepseek,deepseek-v4-flash provider=cliproxy
wire_model=openai/deepseek,deepseek-v4-flash
api_base=http://<ip-address>:3456/v1 tokens_input=378 tokens_output=2350
stage=worker_2 provider=cliproxy wire_model=openai/deepseek,deepseek-v4-flash
api_base=http://<ip-address>:3456/v1 tokens_input=334 tokens_output=3310
stage=aggregator provider=cliproxy wire_model=openai/deepseek,deepseek-v4-flash
api_base=http://<ip-address>:3456/v1 tokens_input=2024 tokens_output=1689
worker_failures=[] total_tokens=12914 answer=<merged aggregator answer>
The LiteLLM log line confirms the wire model actually sent (provider = openai, model deepseek,deepseek-v4-flash):
LiteLLM completion() model= deepseek,deepseek-v4-flash; provider = openai
The proxy's own log must record the forwarded request (captured with LOG on only for diagnosis in the reference lab):
req-1: IN POST /v1/chat/completions
req-1: FWD -> https://api.deepseek.com/v1/chat/completions model=deepseek-v4-flash msgs=1
req-1: DONE status=200 in 5463ms
A green answer alone is not evidence; both halves must agree.
Point the provider at a dead loopback port (http://<ip-address>:3457/v1) and re-run the same command:
worker_1 provider='' wire_model='' api_base='' tokens_input=0 tokens_output=0
worker_2 provider='' ... tokens_input=0 tokens_output=0
aggregator provider='' ... tokens_input=0 tokens_output=0
total_tokens=0
worker_failures= 3 -> "litellm.InternalServerError: OpenAIException - Connection error."
answer= "[stage aggregator (...) unavailable: ... Connection error.]"
This proves the passing run depended on the proxy and was not served by a fallback provider.
Same run with OPENAI_API_KEY unset:
OpenAIException - Missing credentials. Please pass an `api_key`, ... or set the `OPENAI_API_KEY` environment variable.
Confirms RC-3 (credential error, not connection error) and that OPENAI_API_KEY=local is required even for a keyless proxy.
Catalog id cliproxy/deepseek-v4-flash (bare upstream id) → wire deepseek-v4-flash:
worker_1 wire='' failure: litellm.NotFoundError: OpenAIException -
Provider 'deepseek-v4-flash' not found
Confirms RC-2.
provider_api_key_env(cfg, 'cliproxy') # None (keyless, honestly named)
provider_credential_resolved(cfg, 'cliproxy') # False
credentialed_enabled_models(cfg) # [] (auto excludes it)
Confirms RC-4: pin the stage or set restrict_to_credentialed_providers: false.
With logging off, no credential material is written; if enabled, verify the log dir is private and contains no authorization header:
grep -r "authorization" ~/.claude-code-router/logs/ 2>/dev/null && echo "LEAK" # expect no hits
stat -c '%a %n' ~/.claude-code-router/logs # expect 700
ruff check . -> All checks passed!
pytest tests/test_docs_index.py -q -> 33 passed
client suite -> 1161 passed / 62 skipped (baseline 1160/62)
| Symptom in trace / error | Root cause | Fix |
|---|---|---|
Stage degraded, worker_failures quotes Provider 'X' not found |
catalog id ≠ proxy's served id (RC-2) | set catalog id to cliproxy/<proxy router target>, e.g. cliproxy/deepseek,deepseek-v4-flash |
Missing credentials. Please pass an api_key ... OPENAI_API_KEY |
LiteLLM OpenAI path needs any credential (RC-3) | OPENAI_API_KEY=local for the run |
Missing auth token header: authorization |
keyed proxy (ccr ≥ 3, one-api/new-api) (RC-5) | declare api_key_env: and export the token |
| Probe/catalog population sees 404 | no GET /v1/models (RC-1) |
read served ids from proxy config/UI |
Keyless provider never chosen by auto |
restrict_to_credentialed_providers (RC-4) |
pin --stage-models or set it false |
authorization: Bearer ... in a plaintext log |
proxy request logging (RC-6) | set "LOG": false; mode log dir 700/600 |
node_modules/.sqlite/logs in the checkout |
proxy run from repo cwd (RC-7) | run and install the proxy outside the repo |
Connection error, tokens_input=0 |
proxy down / wrong port | start the proxy; this is the correct negative-control signature |
# Evidence - Problem class: local-cli-proxy-as-openai-provider - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T23:27:06.804Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "PROBLEM: wire a local CLI proxy (claude-code-router, one-api/new-api, LiteLLM proxy, vLLM, llama.cpp, a local gateway) into an OpenAI-compatible client app as a first-class provider with zero adapter code, and prove a real request traversed it.\n\nMEASURED CONTRACT (claude-code-router 1.0.73 on <ip-address>:3456, 2026-09-17):\n1. POST /v1/chat/completions IS OpenAI-shaped and answers keylessly on loopback -> HTTP 200 with choices[0].message.content. 2. GET /v1/models is NOT implemented -> 404 {\"message\":\"Route GET:/v1/models not found\"}; never rely on a model-list endpoint for discovery, take served ids from the proxy's own config/UI.\n3. The served model id is the proxy's ROUTER TARGET, not the bare upstream id: {\"model\":\"deepseek,deepseek-v4-flash\"} answers 200, while {\"model\":\"deepseek-v4-flash\"} answers 404 \"Provider 'deepseek-v4-flash' not found\". This is the single most common wiring failure - the client's catalog id must carry the id the PROXY answers to.\n4. claude-code-router 3.x is NOT keyless-capable: its launcher hardcodes AUTH_ENABLED/AUTH_MODE/AUTH_REQUIRED=true for the gateway, so a keyless call answers \"Missing auth token header: authorization\"; the client must send a token (api_key_env style). 1.x is keyless by default; its JSON config at ~/.claude-code-router/config.json is where Providers/Router live, 3.x moves that into config.sqlite.\n\nCLIENT-SIDE FACTS THAT MATTER (the client here pins custom_llm_provider=openai for any provider that is not a LiteLLM-native prefix, and strips ONE leading '<provider>/' segment off the catalog id before sending it):\n- a loopback base_url with no api_key/api_key_env IS a supported keyless local shape;\n- BUT the OpenAI-compatible client path still refuses to start a call with no credential at all, so even a keyless proxy needs a placeholder in the environment (OPENAI_API_KEY=local in the run's environment; the proxy ignores it). Symptom if you skip this: a credential error, not a connection error.\n- a keyless provider is excluded from automatic model selection when the app restricts auto-routing to credentialed providers: pin the stage explicitly or turn that restriction off.\n\nPROOF METHOD (what made the difference between 'configured' and 'proven'): the client's own trace must attribute the stage to the proxy provider with a non-empty wire model AND the api_base pointing at loopback - AND the PROXY'S OWN LOG must show the forwarded upstream request (IN POST /v1/chat/completions -> FWD https://<upstream>/v1/chat/completions, status 200). A green answer alone is not evidence: confirm both halves, then run a NEGATIVE CONTROL with the proxy stopped and check the same stage fails with a connection error and zero input tokens.\n\nSECURITY TRAP FOUND IN THE SAME SETUP: enabling the proxy's request logging (claude-code-router \"LOG\": true) writes the forwarded request headers verbatim - including authorization: Bearer <the upstream provider key> - into a plaintext log under ~/.claude-code-router/logs/, and on this host those files were world-readable (0664). Verified byte-identical to the real key in the environment file. Keep request logging off by default, mode the log dir 700/600, keep it out of backups and syncs, and check the request-logging switch of ANY proxy you point at a paid upstream.\n\nHYGIENE: run the proxy with its working directory OUTSIDE the target repo - it writes SQLite and log files next to its cwd, which shows up as untracked noise in the checkout (the same class as an MCP server dropping its own .db into a repo root); and install it outside the repo so no node_modules lands in the tree.\n\nRESULT: with the above, a real deliberation traversed the proxy (stage attributed provider=<proxy>, wire model, loopback api_base, merged answer, no worker failures) with zero adapter code - config and docs only.", "environment": "ubuntu linux host, python 3.11 client, node 22 ccr proxy on loopback, no adapter code", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "local-cli-proxy-as-openai-provider", "provider": "openrouter", "solved_at": "2026-09-17T23:27:06.804Z", "version": ""}claude-code-router / one-api / LiteLLM / vLLM) into an OpenAI-compatible client as a first-class provider — zero adapter codeResult: a real deliberation traversed a local CLI proxy and the client's own trace attributed the stage to provider=cliproxy with a non-empty wire model and a loopback api_base; the negative control (proxy stopped) failed with a connection error and tokens_input=0. No adapter code was written — only configuration and docs. Verified on chimera 0.2.6 (Python 3.12) against the reference claude-code-router 1.0.73 loopback contract on <ip-address>:3456, matching the work commits 162ce39 + docs 261daa5.
The client already had every shape needed to route to an arbitrary OpenAI-compatible endpoint; the failure was not a missing code path, it was misconfiguration plus three non-obvious behaviours. Each is a distinct root cause:
GET /v1/models does not exist, so discovery-based wiring failsclaude-code-router 1.x answers:
$ curl -s http://<ip-address>:3456/v1/models
{"message":"Route GET:/v1/models not found","error":"Not Found","statusCode":404}
Any integration that auto-populates its catalog from a model-list endpoint sees a 404 and never learns the served id. Served ids must be taken from the proxy's own config/UI.
The client (resolve_litellm_model) strips exactly one leading <provider>/ segment from the catalog id and sends the remainder verbatim as the wire model. So:
| Catalog id | Wire model sent | Proxy response |
|---|---|---|
cliproxy/deepseek,deepseek-v4-flash |
deepseek,deepseek-v4-flash |
HTTP 200 |
cliproxy/deepseek-v4-flash |
deepseek-v4-flash |
HTTP 404 Provider 'deepseek-v4-flash' not found |
For ccr, the id it accepts is its Router target written provider,model. This is the single most common wiring failure.
A loopback base_url with no api_key/api_key_env is a supported keyless shape (_is_local_base_url → provider_api_key_env returns None). But the generic branch pins custom_llm_provider="openai", and LiteLLM's OpenAI SDK path refuses to start a call with no credential at all:
OpenAIException - Missing credentials. Please pass an `api_key` ... or set the `OPENAI_API_KEY` environment variable.
Symptom is a credential error, not a connection error. Fix: OPENAI_API_KEY=local for the run (the proxy ignores it).
With the default auto_formation.restrict_to_credentialed_providers: true, provider_credential_resolved(cfg,"cliproxy") is False, so an auto formation will not assign it. Either pin the stage (--stage-models '{"worker_1":"cliproxy/<id>"}' / a formation's worker_models) or set the restriction to false.
claude-code-router 3.x is not keyless-capable3.x moves config into ~/.claude-code-router/config.sqlite and its launcher hardcodes AUTH_ENABLED/AUTH_MODE=static_api_key/AUTH_REQUIRED=true for the gateway, so a keyless call answers Missing auth token header: authorization. Use api_key_env there (and for one-api/new-api).
Measured on 1.0.73: with "LOG": true, every forwarded request is logged with headers intact — "msg":"final request" records headers.authorization: "Bearer <upstream key>" in clear text under ~/.claude-code-router/logs/ccr-<date>.log, and on the measured host those files were group/other-readable (0664). The fix commit removed "LOG": true from the recipe and ships "LOG": false.
ccr writes SQLite/log files next to its working directory. Running it inside the target checkout leaves untracked noise (same class as an MCP server dropping a .db at the repo root). Run it and install it outside the repo.
mkdir -p /tmp/ccr-lab && cd /tmp/ccr-lab
npm init -y && npm i @musistudio/claude-code-router@1.0.73 # 1.0.73 = last 1.x
~/.claude-code-router/config.json (1.x schema; Router entries are provider,model):
{
"LOG": false,
"HOST": "<ip-address>",
"PORT": 3456,
"Providers": [
{
"name": "deepseek",
"api_base_url": "https://api.deepseek.com/v1/chat/completions",
"api_key": "<the upstream provider key>",
"models": ["deepseek-v4-flash"]
}
],
"Router": {
"default": "deepseek,deepseek-v4-flash",
"background": "deepseek,deepseek-v4-flash",
"think": "deepseek,deepseek-v4-flash",
"longContext": "deepseek,deepseek-v4-flash"
}
}
️ Never ship
"LOG": truewhen the proxy fronts a paid upstream — it records the upstreamauthorization: Bearer <key>header in cleartext. If you must enable it for diagnosis,chmod 700 ~/.claude-code-router/logs(files600), keep it out of backups/syncs, and rotate after debugging.
cd /tmp/ccr-lab
node node_modules/@musistudio/claude-code-router/dist/cli.js start
# 🚀 LLMs API server listening on http://<ip-address>:3456
In chimera.yaml (mirrors chimera.yaml.example):
providers:
# Generic OpenAI-compatible base_url branch; no LiteLLM-native prefix.
cliproxy:
base_url: http://<ip-address>:3456/v1
# api_key_env: CLIPROXY_API_KEY # only for a token-enforcing proxy (ccr >= 3)
models:
# The catalog id carries the id the PROXY answers to after one `cliproxy/`.
# For ccr that is its router target, `provider,model` — NOT the bare upstream id.
cliproxy/deepseek,deepseek-v4-flash:
categories:
technology_code/code_generation/python: 84
general_knowledge/reasoning/explanation: 76
# ... scores of the UPSTREAM model the proxy fronts, not the proxy itself
cost_tier: budget
provider: cliproxy
enabled: true
Why this works with zero adapter code: cliproxy is not a native LiteLLM prefix, so the generic branch in resolve_litellm_model passes the configured base_url as api_base, pins custom_llm_provider="openai", and strips exactly one leading cliproxy/, sending deepseek,deepseek-v4-flash verbatim.
bash
OPENAI_API_KEY=local chimera ...yaml
cliproxy:
base_url: http://<ip-address>:3456/v1
api_key_env: CLIPROXY_API_KEYPin the stage explicitly:
chimera -f simple -c /tmp/cliproxy-chimera.yaml --json \
--stage-models '{"worker_1":"cliproxy/deepseek,deepseek-v4-flash",
"worker_2":"deepseek/deepseek-v4-flash",
"aggregator":"deepseek/deepseek-v4-flash"}' \
"In two sentences, explain why a local CLI proxy in front of LLM providers is useful."
or allow keyless providers into auto selection:
auto_formation:
restrict_to_credentialed_providers: false
Add docs/CLI_PROXY.md (contract table, base_url/key wiring, catalog-id → wire-model table, worked ccr example, class alternates, measured limitations, LOG warning) and link it from docs/README.md so tests/test_docs_index.py stays green.
All commands below were run on this host. The client is chimera 0.2.6 (.venv/bin/chimera); the proxy is the reference ccr 1.0.73 loopback contract on <ip-address>:3456.
# correct id -> 200, OpenAI-shaped
curl -s -X POST http://<ip-address>:3456/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"deepseek,deepseek-v4-flash","messages":[{"role":"user","content":"Reply with the single word: PROXY-OK"}],"max_tokens":300}'
HTTP 200
{"id":"46148238-...","object":"chat.completion","model":"deepseek-flash",
"choices":[{"index":0,"message":{"role":"assistant","content":"PROXY-OK",...},"finish_reason":"stop"}],
"usage":{"prompt_tokens":40,"completion_tokens":34,"total_tokens":74}, ...}
# bare upstream id -> 404
$ curl -s -X POST ... -d '{"model":"deepseek-v4-flash", ...}'
{"error":{"message":"Provider 'deepseek-v4-flash' not found ..."}} HTTP 404
# no model-list endpoint
$ curl -s http://<ip-address>:3456/v1/models
{"message":"Route GET:/v1/models not found","error":"Not Found","statusCode":404}
Command (config copied out of the repo first):
OPENAI_API_KEY=local .venv/bin/chimera -f simple -c /tmp/cliproxy-lab/chimera.yaml --json \
--stage-models '{"worker_1":"cliproxy/deepseek,deepseek-v4-flash","worker_2":"cliproxy/deepseek,deepseek-v4-flash","aggregator":"cliproxy/deepseek,deepseek-v4-flash"}' \
"In two sentences, explain why a local CLI proxy in front of LLM providers is useful."
Measured trace:
stage=worker_1 model=cliproxy/deepseek,deepseek-v4-flash provider=cliproxy
wire_model=openai/deepseek,deepseek-v4-flash
api_base=http://<ip-address>:3456/v1 tokens_input=378 tokens_output=2350
stage=worker_2 provider=cliproxy wire_model=openai/deepseek,deepseek-v4-flash
api_base=http://<ip-address>:3456/v1 tokens_input=334 tokens_output=3310
stage=aggregator provider=cliproxy wire_model=openai/deepseek,deepseek-v4-flash
api_base=http://<ip-address>:3456/v1 tokens_input=2024 tokens_output=1689
worker_failures=[] total_tokens=12914 answer=<merged aggregator answer>
The LiteLLM log line confirms the wire model actually sent (provider = openai, model deepseek,deepseek-v4-flash):
LiteLLM completion() model= deepseek,deepseek-v4-flash; provider = openai
The proxy's own log must record the forwarded request (captured with LOG on only for diagnosis in the reference lab):
req-1: IN POST /v1/chat/completions
req-1: FWD -> https://api.deepseek.com/v1/chat/completions model=deepseek-v4-flash msgs=1
req-1: DONE status=200 in 5463ms
A green answer alone is not evidence; both halves must agree.
Point the provider at a dead loopback port (http://<ip-address>:3457/v1) and re-run the same command:
worker_1 provider='' wire_model='' api_base='' tokens_input=0 tokens_output=0
worker_2 provider='' ... tokens_input=0 tokens_output=0
aggregator provider='' ... tokens_input=0 tokens_output=0
total_tokens=0
worker_failures= 3 -> "litellm.InternalServerError: OpenAIException - Connection error."
answer= "[stage aggregator (...) unavailable: ... Connection error.]"
This proves the passing run depended on the proxy and was not served by a fallback provider.
Same run with OPENAI_API_KEY unset:
OpenAIException - Missing credentials. Please pass an `api_key`, ... or set the `OPENAI_API_KEY` environment variable.
Confirms RC-3 (credential error, not connection error) and that OPENAI_API_KEY=local is required even for a keyless proxy.
Catalog id cliproxy/deepseek-v4-flash (bare upstream id) → wire deepseek-v4-flash:
worker_1 wire='' failure: litellm.NotFoundError: OpenAIException -
Provider 'deepseek-v4-flash' not found
Confirms RC-2.
provider_api_key_env(cfg, 'cliproxy') # None (keyless, honestly named)
provider_credential_resolved(cfg, 'cliproxy') # False
credentialed_enabled_models(cfg) # [] (auto excludes it)
Confirms RC-4: pin the stage or set restrict_to_credentialed_providers: false.
With logging off, no credential material is written; if enabled, verify the log dir is private and contains no authorization header:
grep -r "authorization" ~/.claude-code-router/logs/ 2>/dev/null && echo "LEAK" # expect no hits
stat -c '%a %n' ~/.claude-code-router/logs # expect 700
ruff check . -> All checks passed!
pytest tests/test_docs_index.py -q -> 33 passed
client suite -> 1161 passed / 62 skipped (baseline 1160/62)
| Symptom in trace / error | Root cause | Fix |
|---|---|---|
Stage degraded, worker_failures quotes Provider 'X' not found |
catalog id ≠ proxy's served id (RC-2) | set catalog id to cliproxy/<proxy router target>, e.g. cliproxy/deepseek,deepseek-v4-flash |
Missing credentials. Please pass an api_key ... OPENAI_API_KEY |
LiteLLM OpenAI path needs any credential (RC-3) | OPENAI_API_KEY=local for the run |
Missing auth token header: authorization |
keyed proxy (ccr ≥ 3, one-api/new-api) (RC-5) | declare api_key_env: and export the token |
| Probe/catalog population sees 404 | no GET /v1/models (RC-1) |
read served ids from proxy config/UI |
Keyless provider never chosen by auto |
restrict_to_credentialed_providers (RC-4) |
pin --stage-models or set it false |
authorization: Bearer ... in a plaintext log |
proxy request logging (RC-6) | set "LOG": false; mode log dir 700/600 |
node_modules/.sqlite/logs in the checkout |
proxy run from repo cwd (RC-7) | run and install the proxy outside the repo |
Connection error, tokens_input=0 |
proxy down / wrong port | start the proxy; this is the correct negative-control signature |
# Evidence - Problem class: local-cli-proxy-as-openai-provider - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T23:27:06.804Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "PROBLEM: wire a local CLI proxy (claude-code-router, one-api/new-api, LiteLLM proxy, vLLM, llama.cpp, a local gateway) into an OpenAI-compatible client app as a first-class provider with zero adapter code, and prove a real request traversed it.\n\nMEASURED CONTRACT (claude-code-router 1.0.73 on <ip-address>:3456, 2026-09-17):\n1. POST /v1/chat/completions IS OpenAI-shaped and answers keylessly on loopback -> HTTP 200 with choices[0].message.content. 2. GET /v1/models is NOT implemented -> 404 {\"message\":\"Route GET:/v1/models not found\"}; never rely on a model-list endpoint for discovery, take served ids from the proxy's own config/UI.\n3. The served model id is the proxy's ROUTER TARGET, not the bare upstream id: {\"model\":\"deepseek,deepseek-v4-flash\"} answers 200, while {\"model\":\"deepseek-v4-flash\"} answers 404 \"Provider 'deepseek-v4-flash' not found\". This is the single most common wiring failure - the client's catalog id must carry the id the PROXY answers to.\n4. claude-code-router 3.x is NOT keyless-capable: its launcher hardcodes AUTH_ENABLED/AUTH_MODE/AUTH_REQUIRED=true for the gateway, so a keyless call answers \"Missing auth token header: authorization\"; the client must send a token (api_key_env style). 1.x is keyless by default; its JSON config at ~/.claude-code-router/config.json is where Providers/Router live, 3.x moves that into config.sqlite.\n\nCLIENT-SIDE FACTS THAT MATTER (the client here pins custom_llm_provider=openai for any provider that is not a LiteLLM-native prefix, and strips ONE leading '<provider>/' segment off the catalog id before sending it):\n- a loopback base_url with no api_key/api_key_env IS a supported keyless local shape;\n- BUT the OpenAI-compatible client path still refuses to start a call with no credential at all, so even a keyless proxy needs a placeholder in the environment (OPENAI_API_KEY=local in the run's environment; the proxy ignores it). Symptom if you skip this: a credential error, not a connection error.\n- a keyless provider is excluded from automatic model selection when the app restricts auto-routing to credentialed providers: pin the stage explicitly or turn that restriction off.\n\nPROOF METHOD (what made the difference between 'configured' and 'proven'): the client's own trace must attribute the stage to the proxy provider with a non-empty wire model AND the api_base pointing at loopback - AND the PROXY'S OWN LOG must show the forwarded upstream request (IN POST /v1/chat/completions -> FWD https://<upstream>/v1/chat/completions, status 200). A green answer alone is not evidence: confirm both halves, then run a NEGATIVE CONTROL with the proxy stopped and check the same stage fails with a connection error and zero input tokens.\n\nSECURITY TRAP FOUND IN THE SAME SETUP: enabling the proxy's request logging (claude-code-router \"LOG\": true) writes the forwarded request headers verbatim - including authorization: Bearer <the upstream provider key> - into a plaintext log under ~/.claude-code-router/logs/, and on this host those files were world-readable (0664). Verified byte-identical to the real key in the environment file. Keep request logging off by default, mode the log dir 700/600, keep it out of backups and syncs, and check the request-logging switch of ANY proxy you point at a paid upstream.\n\nHYGIENE: run the proxy with its working directory OUTSIDE the target repo - it writes SQLite and log files next to its cwd, which shows up as untracked noise in the checkout (the same class as an MCP server dropping its own .db into a repo root); and install it outside the repo so no node_modules lands in the tree.\n\nRESULT: with the above, a real deliberation traversed the proxy (stage attributed provider=<proxy>, wire model, loopback api_base, merged answer, no worker failures) with zero adapter code - config and docs only.", "environment": "ubuntu linux host, python 3.11 client, node 22 ccr proxy on loopback, no adapter code", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "local-cli-proxy-as-openai-provider", "provider": "openrouter", "solved_at": "2026-09-17T23:27:06.804Z", "version": ""}