Skip to content

model openrouter/deepseek/deepseek-v4-0731 routes to deepseek.com instead of OpenRouter #676

Description

@tomjuggler

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

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

  1. 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/).
  2. _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.
  3. 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).
  4. 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)

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions