Issue
Running Cecli 1.4.1
Error:
Client error '400 Bad Request' for url 'https://api.deepseek.com/v1/chat/completions'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400
To reproduce: run with
cecli --agent-config '{"command_timeout": 0, "skip_cli_confirmations": true}' --model openrouter/deepseek/deepseek-v4-flash-0731 --editor-model openrouter/deepseek/deepseek-v4-flash-0731
Work-around (AI Generated bug report, suggestions with work-around, confirmed to work by me):
Bug report: openrouter/<vendor>/<model> slugs missing from bundled metadata misroute to the bare vendor's API, and the prefixed slug is rejected by OpenRouter
Environment
- cecli 1.4.1 (installed via
uv tool install)
- Model:
openrouter/deepseek/deepseek-v4-flash-0731 (valid OpenRouter slug, verified against GET https://openrouter.ai/api/v1/models)
OPENROUTER_API_KEY set; DEEPSEEK_API_KEY set
Symptoms
cecli --model openrouter/deepseek/deepseek-v4-flash-0731 →
400 Bad Request against https://api.deepseek.com/v1/chat/completions
(request went to DeepSeek's API with DeepSeek auth, not OpenRouter).
- After pinning
api_base to OpenRouter via model-settings extra_params →
404 Not Found from https://openrouter.ai/api/v1/chat/completions.
Root cause (verified by tracing cecli 1.4.1 source and capturing the outgoing HTTP request)
Defect 1: provider misroute via metadata fallback
Model.__init__ → get_default_config() scans bundled
cecli/resources/model-metadata.json. There is no record for
openrouter/deepseek/deepseek-v4-flash-0731 (openrouter's catalogued
deepseek keys stop at deepseek-v4-pro; dated snapshots exist only under
azure_ai/, dashscope/, databricks/).
_find_record (cecli/helpers/model_config/pipeline.py) falls back by
progressively shortening the route and dropping the provider prefix,
landing on the bare deepseek-v4-flash record, whose
litellm_provider: deepseek.
resolve_model_config() (cecli/helpers/llms/config.py) takes the
record's litellm_provider over the user's explicit openrouter/ prefix
(the prefix-wins guards only cover github_copilot / bedrock / claude
models / providers known to ModelProviderManager.supports_provider() —
openrouter has no entry in providers.json).
- Result:
provider=deepseek, api_base=https://api.deepseek.com/v1,
api_key_env=DEEPSEEK_API_KEY → 400 from the wrong API.
Defect 2: payload model ID keeps the openrouter/ prefix (404 from OpenRouter)
-
resolve_model_config computes
route = model.split("/", 1)[1] — it strips only one prefix segment,
so for openrouter/deepseek/deepseek-v4-flash-0731 the payload model
becomes deepseek/deepseek-v4-flash-0731… but only when the resolved
provider is not openrouter. When the provider is openrouter
(e.g. for catalogued models), the same single-split logic yields
deepseek/deepseek-v4-flash-0731, which is correct — however the
dispatcher's chat payload (cecli/helpers/llms/domains/chat.py:51,
"model": resolved["route"]) combined with the defective fallback in
Defect 1 means a user who forces the request to OpenRouter (via
extra_params.api_base) still sends a payload the endpoint rejects,
because the route/payload is derived from the broken resolution chain
rather than from OpenRouter's actual model-ID format.
Empirically (HTTP capture of cecli's real dispatch stack):
- with the DeepSeek fallback active, the payload model is
deepseek/deepseek-v4-flash-0731 but the host is
api.deepseek.com → 400;
- forcing
api_base=https://openrouter.ai/api/v1 via extra_params
sends model: openrouter/deepseek/deepseek-v4-flash-0731 in the body →
OpenRouter replies 404 Not Found (their API requires model IDs
without the openrouter/ prefix; a deliberately prefixed request
returns 400 "openrouter/... is not a valid model ID", while an
unauthenticated one returns 401/404 depending on validation order).
Confirmed against the live OpenRouter API with a real key:
model: "deepseek/deepseek-v4-flash-0731" → 200, completion returned
(served by provider "Reka").
model: "openrouter/deepseek/deepseek-v4-flash-0731" →
400 "not a valid model ID".
Net effect: there is no configuration of the model name that both
(a) resolves to OpenRouter's endpoint and (b) carries a payload model ID
OpenRouter accepts — the two halves of the resolution disagree.
Defect 3 (minor): user metadata overrides cannot fix routing
resolve_model_config() calls get_default_config(model) with bundled
metadata only (ModelInfoManager.get_metadata_sources() is bypassed), so
records supplied via --model-metadata-file / .cecli.model.metadata.json
are loaded into local_model_metadata for model info (context window,
costs) but never influence provider/api_base resolution. Metadata overrides
therefore cannot repair the misroute; only extra_params/provider config can.
Suggested fix
In _find_record / resolve_model_config:
- When the model name carries a provider prefix, restrict fallback records
to that prefix (do not fall back to a bare-vendor record that the prefix
does not name), or synthesize litellm_provider = prefix for known
aggregator prefixes such as openrouter/ (their wire format is always
https://openrouter.ai/api/v1 + Bearer OPENROUTER_API_KEY).
- Route/payload derivation: ensure the payload model ID is the OpenRouter
catalog ID (no openrouter/ prefix) whenever the resolved provider is
openrouter, independently of how the provider was resolved.
- Feed user-supplied metadata files (
ModelInfoManager.get_metadata_sources())
into resolve_model_config so a user record can pin litellm_provider.
Workaround (verified, reversible — no source changes)
Register OpenRouter as an explicit provider in ~/.cecli.conf.yml (or
.cecli.conf.yml in the project). Per cecli/helpers/llms/config.py, a
configured provider wins over the metadata record's litellm_provider for
every model under its prefix:
model-providers:
openrouter:
api_base: "https://openrouter.ai/api/v1"
api_key_env:
- "OPENROUTER_API_KEY"
display_name: "OpenRouter"
Command line stays unchanged:
cecli --model openrouter/deepseek/deepseek-v4-flash-0731
Verified end-to-end by capturing cecli's actual outgoing request (local HTTP
sink on the real dispatch path):
DISPATCH resolved: provider=openrouter
route=deepseek/deepseek-v4-flash-0731
api_base=https://openrouter.ai/api/v1
api_key_env=OPENROUTER_API_KEY
captured request: POST <base>/chat/completions
payload model: deepseek/deepseek-v4-flash-0731 (bare OpenRouter ID)
auth: Bearer <OPENROUTER_API_KEY>
This works because cecli itself strips the single openrouter/ prefix when
building the payload once the provider resolves to openrouter, and the
prefix-based record fallback no longer lands on the bare deepseek record.
Reversibility: delete the model-providers: block once upstream ships
metadata records for these slugs. The entry is harmless to keep permanently.
An earlier workaround (~/.cecli.model.settings.yml with
extra_params.api_base) is NOT sufficient — it fixes the URL but not the
payload model ID, and fails with 404; do not use it.
Summary of defects
| # |
Defect |
Effect |
| 1 |
Metadata gap: no record for openrouter/deepseek/deepseek-v4-flash-0731 (and other dated OpenRouter-only snapshots) |
Fallback match to bare deepseek-v4-flash |
| 2 |
Provider fallback drops the openrouter/ prefix and trusts the fallback record's litellm_provider |
Request goes to api.deepseek.com with DEEPSEEK_API_KEY → 400 |
| 3 |
Payload model ID and endpoint resolution disagree under override (extra_params can fix URL but not body) |
404 from openrouter.ai (prefixed ID in body) |
| 4 |
resolve_model_config ignores user --model-metadata-file entries |
Metadata overrides cannot repair routing |
Version and model info
cecli v1.4.1
openrouter/deepseek/deepseek-v4-0731
Issue
Running Cecli 1.4.1
Error:
To reproduce: run with
Work-around (AI Generated bug report, suggestions with work-around, confirmed to work by me):
Bug report:
openrouter/<vendor>/<model>slugs missing from bundled metadata misroute to the bare vendor's API, and the prefixed slug is rejected by OpenRouterEnvironment
uv tool install)openrouter/deepseek/deepseek-v4-flash-0731(valid OpenRouter slug, verified againstGET https://openrouter.ai/api/v1/models)OPENROUTER_API_KEYset;DEEPSEEK_API_KEYsetSymptoms
cecli --model openrouter/deepseek/deepseek-v4-flash-0731→400 Bad Requestagainsthttps://api.deepseek.com/v1/chat/completions(request went to DeepSeek's API with DeepSeek auth, not OpenRouter).
api_baseto OpenRouter via model-settingsextra_params→404 Not Foundfromhttps://openrouter.ai/api/v1/chat/completions.Root cause (verified by tracing cecli 1.4.1 source and capturing the outgoing HTTP request)
Defect 1: provider misroute via metadata fallback
Model.__init__→get_default_config()scans bundledcecli/resources/model-metadata.json. There is no record foropenrouter/deepseek/deepseek-v4-flash-0731(openrouter's catalogueddeepseek keys stop at
deepseek-v4-pro; dated snapshots exist only underazure_ai/,dashscope/,databricks/)._find_record(cecli/helpers/model_config/pipeline.py) falls back byprogressively shortening the route and dropping the provider prefix,
landing on the bare
deepseek-v4-flashrecord, whoselitellm_provider: deepseek.resolve_model_config()(cecli/helpers/llms/config.py) takes therecord's
litellm_providerover the user's explicitopenrouter/prefix(the prefix-wins guards only cover
github_copilot/bedrock/ claudemodels / providers known to
ModelProviderManager.supports_provider()—openrouterhas no entry inproviders.json).provider=deepseek,api_base=https://api.deepseek.com/v1,api_key_env=DEEPSEEK_API_KEY→ 400 from the wrong API.Defect 2: payload model ID keeps the
openrouter/prefix (404 from OpenRouter)resolve_model_configcomputesroute = model.split("/", 1)[1]— it strips only one prefix segment,so for
openrouter/deepseek/deepseek-v4-flash-0731the payload modelbecomes
deepseek/deepseek-v4-flash-0731… but only when the resolvedprovider is not
openrouter. When the provider isopenrouter(e.g. for catalogued models), the same single-split logic yields
deepseek/deepseek-v4-flash-0731, which is correct — however thedispatcher's chat payload (
cecli/helpers/llms/domains/chat.py:51,"model": resolved["route"]) combined with the defective fallback inDefect 1 means a user who forces the request to OpenRouter (via
extra_params.api_base) still sends a payload the endpoint rejects,because the route/payload is derived from the broken resolution chain
rather than from OpenRouter's actual model-ID format.
Empirically (HTTP capture of cecli's real dispatch stack):
deepseek/deepseek-v4-flash-0731but the host isapi.deepseek.com→ 400;api_base=https://openrouter.ai/api/v1viaextra_paramssends
model: openrouter/deepseek/deepseek-v4-flash-0731in the body →OpenRouter replies
404 Not Found(their API requires model IDswithout the
openrouter/prefix; a deliberately prefixed requestreturns
400 "openrouter/... is not a valid model ID", while anunauthenticated one returns 401/404 depending on validation order).
Confirmed against the live OpenRouter API with a real key:
model: "deepseek/deepseek-v4-flash-0731"→ 200, completion returned(served by provider "Reka").
model: "openrouter/deepseek/deepseek-v4-flash-0731"→400 "not a valid model ID".Net effect: there is no configuration of the model name that both
(a) resolves to OpenRouter's endpoint and (b) carries a payload model ID
OpenRouter accepts — the two halves of the resolution disagree.
Defect 3 (minor): user metadata overrides cannot fix routing
resolve_model_config()callsget_default_config(model)with bundledmetadata only (
ModelInfoManager.get_metadata_sources()is bypassed), sorecords supplied via
--model-metadata-file/.cecli.model.metadata.jsonare loaded into
local_model_metadatafor model info (context window,costs) but never influence provider/api_base resolution. Metadata overrides
therefore cannot repair the misroute; only
extra_params/provider config can.Suggested fix
In
_find_record/resolve_model_config:to that prefix (do not fall back to a bare-vendor record that the prefix
does not name), or synthesize
litellm_provider = prefixfor knownaggregator prefixes such as
openrouter/(their wire format is alwayshttps://openrouter.ai/api/v1+ BearerOPENROUTER_API_KEY).catalog ID (no
openrouter/prefix) whenever the resolved provider isopenrouter, independently of how the provider was resolved.ModelInfoManager.get_metadata_sources())into
resolve_model_configso a user record can pinlitellm_provider.Workaround (verified, reversible — no source changes)
Register OpenRouter as an explicit provider in
~/.cecli.conf.yml(or.cecli.conf.ymlin the project). Percecli/helpers/llms/config.py, aconfigured provider wins over the metadata record's
litellm_providerforevery model under its prefix:
Command line stays unchanged:
Verified end-to-end by capturing cecli's actual outgoing request (local HTTP
sink on the real dispatch path):
This works because cecli itself strips the single
openrouter/prefix whenbuilding the payload once the provider resolves to
openrouter, and theprefix-based record fallback no longer lands on the bare deepseek record.
Reversibility: delete the
model-providers:block once upstream shipsmetadata records for these slugs. The entry is harmless to keep permanently.
An earlier workaround (
~/.cecli.model.settings.ymlwithextra_params.api_base) is NOT sufficient — it fixes the URL but not thepayload model ID, and fails with 404; do not use it.
Summary of defects
openrouter/deepseek/deepseek-v4-flash-0731(and other dated OpenRouter-only snapshots)deepseek-v4-flashopenrouter/prefix and trusts the fallback record'slitellm_providerapi.deepseek.comwithDEEPSEEK_API_KEY→ 400openrouter.ai(prefixed ID in body)resolve_model_configignores user--model-metadata-fileentriesVersion and model info
cecli v1.4.1
openrouter/deepseek/deepseek-v4-0731