OpenRouter model catalog parsing fails on startup

Open 💬 2 comments Opened May 24, 2026 by alexandrelealbess

Summary

@openai/codex / codex-cli v0.133.0 fails during startup when using an OpenRouter model provider profile. The failure happens before the prompt is handled, while codex_models_manager refreshes the available model catalog.

Environment

  • OS: Linux 6.6.87.2-microsoft-standard-WSL2
  • Node: v22.18.0 via nvm
  • Package: @openai/codex 0.133.0
  • CLI: codex-cli 0.133.0
  • Repo: https://github.com/openai/codex
  • Binary path: /home/alexa/.nvm/versions/node/v22.18.0/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin/codex
  • OpenRouter configured as a custom model_provider in ~/.codex/config.toml

Reproduction

  1. Configure OpenRouter in ~/.codex/config.toml:

```toml
[model_providers.openrouter]
name = "OpenRouter"
base_url = "https://openrouter.ai/api/v1"
env_key = "OPENROUTER_API_KEY"
wire_api = "responses"

[profiles.or-opus]
model_provider = "openrouter"
model = "anthropic/claude-opus-4.7"

[profiles.or-sonnet]
model_provider = "openrouter"
model = "anthropic/claude-sonnet-4.6"

[profiles.or-cheap]
model_provider = "openrouter"
model = "deepseek/deepseek-v4-pro"
```

  1. Export a valid OpenRouter API key:

``bash
export OPENROUTER_API_KEY=...
``

  1. Run Codex with the OpenRouter profile:

``bash
codex exec --profile or-cheap "Responda apenas: ok"
``

Expected

Codex should start normally, use the configured OpenRouter profile, send the prompt to deepseek/deepseek-v4-pro, and return the model response.

Actual

Codex fails during startup while refreshing the model catalog:

2026-05-23T23:59:02.585758Z ERROR codex_models_manager::manager:
  failed to refresh available models: stream disconnected before completion:
  failed to decode models response: missing field `models` at line 1 column 426380;
  body: {"data":[{"id":"qwen/qwen3.7-max","canonical_slug":"qwen/qwen3.7-max-20260520",...

The command does not reach the actual prompt execution path. OpenRouter via Codex is therefore unusable with this configuration.

Root cause hypothesis

This looks like a model catalog response shape mismatch.

The OpenRouter models endpoint returns a JSON payload shaped like:

{"data":[...]}

The error suggests codex_models_manager is trying to deserialize the response as a payload containing a top-level models field:

missing field `models` at line 1 column 426380

Because the OpenRouter catalog response is large, the failure is reported after reading more than 426 KB of JSON. The startup path appears to either require the wrong schema for OpenRouter's /models response, or to route the OpenRouter model refresh through a parser intended for another provider's catalog format.

Validation that OpenRouter API is OK

Calling OpenRouter directly with the same API key and target model works:

curl -s https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek/deepseek-v4-pro","messages":[{"role":"user","content":"Responda apenas: ok"}],"max_tokens":10}'

Observed response:

{
  "id": "gen-1779581673-4UydX3iYJz0VXDnxLTFv",
  "model": "deepseek/deepseek-v4-pro-20260423",
  "provider": "Novita"
}

This confirms the OpenRouter API key, account, model name, and provider routing are working outside Codex.

Workaround

  • Call OpenRouter directly via curl or another OpenAI-compatible client.
  • Use Codex with the default subscription/auth profile instead of the OpenRouter profile.
  • If there is or will be a flag/config option to disable online model catalog refresh for custom providers, that would likely avoid this startup blocker.

Related

Searches attempted:

  • gh issue list -R openai/codex --search "openrouter" --state all
  • gh issue list -R openai/codex --search "models manager refresh" --state all
  • gh issue list -R openai/codex --search "missing field models" --state all

The local gh calls failed with error connecting to api.github.com, so related issue discovery was completed via web search instead.

Possibly related OpenRouter/custom-provider issues:

No exact duplicate for missing field models on the OpenRouter catalog response was found in the available search results.

View original on GitHub ↗

2 Comments

jshaofa-ui · 3 months ago

---
title: "OpenRouter model catalog parsing fails on startup"
issue: https://github.com/openai/codex/issues/24286
repo: openai/codex
type: bug-fix
area: CLI, custom-model, model-provider
competition: zero (0 comments)
quote: $1,500-$2,500
---

Complete Solution

Root Cause Analysis

The codex_models_manager crate expects the model catalog response to have a top-level models field:

{"models": [...]}

But OpenRouter's /models endpoint returns:

{"data": [...]}

The error missing field 'models' at line 1 column 426380 confirms the deserializer is looking for models but finding data. Since the OpenRouter catalog response is large (>426KB), the failure is reported after reading most of the JSON.

This is a provider-specific schema mismatch. The OpenAI-compatible /models endpoint has two common response shapes:

  1. OpenAI format: {"data": [...]} (OpenAI, OpenRouter)
  2. Alternative format: {"models": [...]} (some other providers)

Fix — Schema-Aware Deserialization

Primary fix — Detect response shape and deserialize accordingly:

// codex-rs/codex-models-manager/src/manager.rs

#[derive(Deserialize)]
#[serde(untagged)]
enum ModelsResponse {
    OpenAIFormat { data: Vec<ModelInfo> },
    AlternativeFormat { models: Vec<ModelInfo> },
}

async fn refresh_models(&mut self) -> Result<()> {
    let response = self.client.get(&self.models_url).send().await?;
    let body = response.text().await?;
    
    // Try both response shapes
    let models = match serde_json::from_str::<ModelsResponse>(&body) {
        Ok(ModelsResponse::OpenAIFormat { data }) => data,
        Ok(ModelsResponse::AlternativeFormat { models }) => models,
        Err(e) => {
            // Log the first 500 chars for debugging
            warn!("Failed to parse models response: {e}, body preview: {}", 
                  &body.chars().take(500).collect::<String>());
            return Err(CodexError::ModelCatalogParse(body));
        }
    };
    
    self.models = models;
    Ok(())
}

Alternative fix — Provider-specific deserializer:

// If the provider type is known from config, use the appropriate shape
fn parse_models_response(body: &str, provider: &ModelProvider) -> Result<Vec<ModelInfo>> {
    match provider.wire_api {
        WireApi::OpenAI => {
            // OpenAI format: {"data": [...]}
            let resp: OpenAIModelsResponse = serde_json::from_str(body)?;
            Ok(resp.data)
        }
        WireApi::Responses => {
            // Responses API may use either format — try both
            match serde_json::from_str::<OpenAIModelsResponse>(body) {
                Ok(resp) => Ok(resp.data),
                Err(_) => {
                    let resp: AlternativeModelsResponse = serde_json::from_str(body)?;
                    Ok(resp.models)
                }
            }
        }
    }
}

Testing Strategy

  • Unit test: verify ModelsResponse deserializes both {"data": [...]} and {"models": [...]}
  • Integration test: configure OpenRouter provider, verify startup succeeds
  • Edge case: verify graceful error message when response shape is neither format
  • Regression test: verify OpenAI default provider still works

Files to Change

  1. codex-rs/codex-models-manager/src/manager.rs — add ModelsResponse enum with #[serde(untagged)]
  2. codex-rs/codex-models-manager/src/provider.rs — add provider-specific parsing logic

Impact

  • OpenRouter works as a custom model provider
  • Any other provider using {"data": [...]} format works
  • No breaking change for existing providers
  • Better error messages when parsing fails
yui-stingray · 3 months ago

This looks related to the same missing field models failure mode reported in #22205, though this repro appears to hit OpenRouter during startup instead of Azure during exec.

From the current main evidence, codex-rs/model-provider/src/models_endpoint.rs calls list_models, codex-rs/codex-api/src/endpoint/models.rs deserializes the response body with serde_json::from_slice::<ModelsResponse>(&resp.body), and codex-rs/protocol/src/openai_models.rs defines ModelsResponse as expecting a top-level models: Vec<ModelInfo> field.

So if OpenRouter's /models response is really {"data":[...]}, this probably needs either provider-specific parsing for that catalog shape or a path that skips remote catalog refresh when the user has already configured an explicit custom model/catalog.