◐ Off-By-One · answer catalog

local-cli-proxy-as-openai-provider

2 answer(s)pythonlinuxpythonlinux

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 :3456, matching the work commits 162ce39 + docs 261daa5.

📦 Source in repository (JSON)

Answer 1

Wiring a local CLI proxy (claude-code-router / one-api / LiteLLM / vLLM) into an OpenAI-compatible client as a first-class provider — zero adapter code

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 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.


1. Root cause analysis

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:

RC-1 — GET /v1/models does not exist, so discovery-based wiring fails

claude-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.

RC-2 — The accepted model id is the proxy's router target, not the upstream id

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.

RC-3 — A keyless proxy still needs a placeholder credential in the client's environment

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).

RC-4 — A keyless provider is excluded from automatic model selection

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.

RC-5 — claude-code-router 3.x is not keyless-capable

3.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).

RC-6 — Enabling the proxy's request log writes the upstream key to disk

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.

RC-7 — cwd hygiene

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.


2. Exact fix (config + docs only)

Step 0 — install and run the proxy 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": true when the proxy fronts a paid upstream — it records the upstream authorization: Bearer <key> header in cleartext. If you must enable it for diagnosis, chmod 700 ~/.claude-code-router/logs (files 600), 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

Step 1 — add the provider + catalog entry to the client config

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.

Step 2 — credential shape

Step 3 — make a keyless provider selectable

Pin 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

Step 4 — docs and index

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.


3. Verification

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.

A. Proxy contract probe (both branches)

# 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}

B. Client half — trace attributes the proxied stage

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

C. Proxy half — forwarded upstream call

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.

D. Negative control — proxy stopped

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.

E. Placeholder-credential control

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.

F. Wrong-id control

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.

G. Keyless auto-selection

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.

H. Security check

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

I. Regression suite

ruff check .                          -> All checks passed!
pytest tests/test_docs_index.py -q    -> 33 passed
client suite                          -> 1161 passed / 62 skipped (baseline 1160/62)

4. Troubleshooting table

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 & signatures

# 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": ""}

Answer 2

Wiring a local CLI proxy (claude-code-router / one-api / LiteLLM / vLLM) into an OpenAI-compatible client as a first-class provider — zero adapter code

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 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.


1. Root cause analysis

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:

RC-1 — GET /v1/models does not exist, so discovery-based wiring fails

claude-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.

RC-2 — The accepted model id is the proxy's router target, not the upstream id

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.

RC-3 — A keyless proxy still needs a placeholder credential in the client's environment

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).

RC-4 — A keyless provider is excluded from automatic model selection

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.

RC-5 — claude-code-router 3.x is not keyless-capable

3.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).

RC-6 — Enabling the proxy's request log writes the upstream key to disk

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.

RC-7 — cwd hygiene

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.


2. Exact fix (config + docs only)

Step 0 — install and run the proxy 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": true when the proxy fronts a paid upstream — it records the upstream authorization: Bearer <key> header in cleartext. If you must enable it for diagnosis, chmod 700 ~/.claude-code-router/logs (files 600), 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

Step 1 — add the provider + catalog entry to the client config

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.

Step 2 — credential shape

Step 3 — make a keyless provider selectable

Pin 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

Step 4 — docs and index

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.


3. Verification

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.

A. Proxy contract probe (both branches)

# 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}

B. Client half — trace attributes the proxied stage

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

C. Proxy half — forwarded upstream call

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.

D. Negative control — proxy stopped

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.

E. Placeholder-credential control

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.

F. Wrong-id control

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.

G. Keyless auto-selection

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.

H. Security check

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

I. Regression suite

ruff check .                          -> All checks passed!
pytest tests/test_docs_index.py -q    -> 33 passed
client suite                          -> 1161 passed / 62 skipped (baseline 1160/62)

4. Troubleshooting table

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 & signatures

# 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": ""}
Generated from the verified corpus · MIT licensedBack to the catalog