Skip to content

[Bug]: a routing profile candidate naming a disabled provider invalidates the entire config #1785

Description

@brunoflma

Client or integration

Direct HTTP/API client

Area

Proxy and routing

Summary

A single routingProfiles candidate naming a disabled provider invalidates the entire config.json. The loader falls back to the built-in default, so the proxy keeps serving — but with only the openai provider, none of the operator's API keys, and no modelCosts. Every other provider, profile, and price row silently disappears.

The failure is hard to notice because nothing turns red. /healthz returns 200, the management API returns 200, and ocx status reports a healthy proxy. The only visible symptom is unrelated-looking: a routing-profile dry-run returns 404 unknown_profile, which reads like a broken profile rather than a discarded config.

The validation itself is correct and worth keeping — a candidate pointing at a disabled provider is a real mistake. What seems disproportionate is the blast radius: one bad candidate in one profile discards the whole document. Dropping the offending candidate (or the offending profile) with a startup warning would preserve everything else and still surface the mistake.

Mechanically, routingProfileIssues() feeds ctx.addIssue({ code: "custom", ... }) inside the Zod superRefine in src/config.ts (~line 1633), so a candidate-level problem is raised as a document-level parse failure. Combos are validated the same way immediately above it, so the same shape likely applies there.

Reproduction

Verified on 2.20.0. A provider being disabled is the trigger, so any disabled provider works.

  1. In config.json, mark any configured provider "disabled": true — for example providers.TR.
  2. Add a routing profile whose candidate list names that provider:
"routingProfiles": {
  "example": {
    "candidates": [
      { "provider": "TR", "model": "moonshotai/kimi-k3" }
    ]
  }
}
  1. Restart the proxy: ocx restart && ocx ensure
  2. Observe that the proxy starts and reports healthy.

Observed:

  • The service log records Could not load opencodex config ... Using default config.
  • A config.json.invalid-* file is written on each load attempt (10 accumulated here before it was noticed).
  • GET /api/providers returns 1 provider (openai) while config.json on disk defines 11.
  • POST /api/routing-profiles/dry-run returns 404 unknown_profile.
  • /healthz and the management API return 200 throughout.

Expected: the invalid candidate (or the profile containing it) is rejected with a warning naming the profile and provider, and the rest of the config still loads.

A cheap check that distinguishes "config loaded" from "silently fell back to default" is comparing the provider count on disk against the served one:

node -e "const fs=require('fs'),h=require('http');const T=fs.readFileSync(process.env.USERPROFILE+'/.opencodex/admin-api-token','utf8').trim();const n=Object.keys(JSON.parse(fs.readFileSync(process.env.USERPROFILE+'/.opencodex/config.json','utf8')).providers).length;h.get({host:'127.0.0.1',port:10100,path:'/api/providers',headers:{authorization:'Bearer '+T}},r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{const a=JSON.parse(d);const m=(Array.isArray(a)?a:a.data||a.providers||[]).length;console.log('disk',n,'served',m,n===m?'OK':'CONFIG REJECTED')})})"

Version

2.20.0

Operating system

Windows 11 Home Single Language 26200

Provider and model

Not provider-specific — reproduced with a disabled provider of any kind.

Logs or error output

Could not load opencodex config (see config.json.invalid-*). Using default config

# config.json on disk defines 11 providers; the served config has 1
$ curl -s -H "authorization: Bearer $TOKEN" http://127.0.0.1:10100/api/providers | jq 'length'
1

$ curl -s -X POST -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
    -d '{"profile":"example","evidence":{}}' \
    http://127.0.0.1:10100/api/routing-profiles/dry-run
{"error":"unknown_profile"}

$ curl -s http://127.0.0.1:10100/healthz
{"ok":true}

Redacted configuration

{
  "providers": {
    "TR": { "adapter": "openai-chat", "apiKey": "REDACTED", "disabled": true }
  },
  "routingProfiles": {
    "example": {
      "candidates": [
        { "provider": "TR", "model": "moonshotai/kimi-k3" }
      ]
    }
  }
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions