Skip to content

[Bug][2.36.0] Live discovery drops a callable configured model: isDatedVariantId only matches YYYYMMDD and only folds base->dated #3024

Description

@kaicot

Client or integration

Direct HTTP/API client (also visible in the OpenCodex dashboard and ocx models live)

Area

Catalog / models

Summary

mergeConfiguredModelsIntoLiveCatalog() has a dated-alias folding path whose job is to keep a configured model id in the catalog when live discovery returns the same model under a different, date-suffixed id. That fold is implemented by isDatedVariantId(), which is wrong in two independent ways:

  1. It only accepts an 8-digit YYYYMMDD suffix. Providers that use a 4-digit MMDD suffix (Alibaba Token Plan / DeepSeek: deepseek-v4-pro-0813, deepseek-v4-flash-0731) never match.
  2. It only folds configured = base -> live = dated. The reverse case (configured = dated, live = base) is not handled at all.

The result: a model that is on the account's plan, present in providers.<name>.models, and verifiably callable is silently removed from the authoritative live catalog, so it cannot be selected in the dashboard, in ocx models live, or in the Codex model picker. GET /api/providers still reports discovery: { "status": "ok" }, so nothing on the API surface indicates a model was dropped.

I expected the configured id to survive, because upstream does return this model — just under its base id (deepseek-v4-pro) — which is exactly the situation the dated-alias fold exists to handle.

Note this is not the same request as #1690. #1690 asks for a new opt-in retainModels field to retain models that upstream genuinely does not return, and the maintainer review there correctly notes that retaining unconditionally would resurrect dead rows. This report is narrower: the existing fold already requires a matching live row to be present before it retains anything, so fixing it cannot resurrect a model the upstream does not advertise.

Reproduction

  1. Configure alibaba-token-plan-intl (international Token Plan, ap-southeast-1) with liveModels: true and a models list that matches the plan's text/reasoning models, including deepseek-v4-pro-0813.

  2. Confirm the configured list is intact:

    GET /api/providers  ->  models: [ ..., "deepseek-v4-pro", "deepseek-v4-pro-0813", ... ]   (9 ids)
    
  3. Run live discovery:

    ocx models live --provider alibaba-token-plan-intl
    
  4. Observe that deepseek-v4-pro-0813 is gone, while deepseek-v4-pro remains. GET /api/models shows the same 10 rows.

  5. Confirm the dropped model is actually callable, both directly upstream and through the proxy (see logs below). Both return HTTP 200.

Version

2.36.0

Operating system

Windows 11 Pro 26200

Provider and model

alibaba-token-plan-intl / deepseek-v4-pro-0813

Logs or error output

# 1) Configured models via management API -- 9 ids, includes the dated one
GET /api/providers  ->  alibaba-token-plan-intl
  liveModels = true
  models = qwen3.8-max, qwen3.7-max, qwen3.7-plus, qwen3.6-flash,
           deepseek-v4-pro, deepseek-v4-pro-0813, glm-5.2,
           deepseek-v4-flash-0731, qwen3.8-flash
  discovery = { "status": "ok" }        # no indication anything was dropped

# 2) Upstream GET /models (public Token Plan intl endpoint) -- 12 ids
#    NOTE: it advertises the BASE id only.
deepseek-v4-flash-0731
deepseek-v4-pro                 <-- base id present
glm-5.2
qwen-audio-3.0-realtime-plus
qwen-audio-3.0-tts-plus
qwen3.6-flash
qwen3.7-max
qwen3.7-plus
qwen3.8-flash
qwen3.8-max
wan2.7-image                    (filtered out by OpenCodex: image-generation)
wan2.7-image-pro                (filtered out by OpenCodex: image-generation)
# deepseek-v4-pro-0813 is NOT advertised upstream

# 3) ocx models live --provider alibaba-token-plan-intl   -- 10 rows, dated id dropped
alibaba-token-plan-intl/deepseek-v4-flash-0731  [routed, disabled]
alibaba-token-plan-intl/deepseek-v4-pro  [routed, disabled]
alibaba-token-plan-intl/glm-5.2  [routed, disabled]
alibaba-token-plan-intl/qwen-audio-3.0-realtime-plus  [routed, disabled]
alibaba-token-plan-intl/qwen-audio-3.0-tts-plus  [routed, disabled]
alibaba-token-plan-intl/qwen3.6-flash  [routed, disabled]
alibaba-token-plan-intl/qwen3.7-max  [routed, disabled]
alibaba-token-plan-intl/qwen3.7-plus  [routed, disabled]
alibaba-token-plan-intl/qwen3.8-flash  [routed, disabled]
alibaba-token-plan-intl/qwen3.8-max  [routed, enabled]
# deepseek-v4-pro-0813 absent

# 4) The dropped model is callable -- DIRECT to upstream
POST <token-plan intl baseUrl>/chat/completions
Authorization: Bearer <redacted>
{"model":"deepseek-v4-pro-0813","messages":[{"role":"user","content":"Reply with exactly: OK"}],"max_tokens":32}
-> HTTP 200   model="deepseek-v4-pro-0813"   content="OK"

# 5) The dropped model is callable -- THROUGH the OpenCodex proxy
POST /v1/chat/completions
{"model":"alibaba-token-plan-intl/deepseek-v4-pro-0813","messages":[{"role":"user","content":"Reply with exactly: OK"}],"max_tokens":32}
-> HTTP 200   model="deepseek-v4-pro-0813"   content="OK"

# So routing by explicit namespace still works; only catalog/picker visibility is lost.

Root cause

src/codex/catalog/provider-fetch.ts:938 (identical on main as of this report):

export function isDatedVariantId(liveId: string, configuredId: string): boolean {
  if (!liveId.startsWith(`${configuredId}-`)) return false;
  return /^\d{8}$/.test(liveId.slice(configuredId.length + 1));
}

Used by the fold in mergeConfiguredModelsIntoLiveCatalog() at src/codex/catalog/provider-fetch.ts:1668-1686:

for (const candidate of configured) {
  if (present.has(candidate.id)) continue;
  const dated = out.find(live => isDatedVariantId(live.id, candidate.id));
  if (dated) { /* retain candidate.id, with provider config hints applied */ }
  ...
  droppedConfiguredIds.push(candidate.id);
}

For configured = "deepseek-v4-pro-0813", live = "deepseek-v4-pro":

  • liveId.startsWith("deepseek-v4-pro-0813-") is false -> no match (direction).
  • Even with the arguments swapped, the slice yields "0813", and /^\d{8}$/ rejects 4 digits (format).

Both guards fail, so the id falls through to droppedConfiguredIds. The only remaining retention path is shouldRetainConfiguredProviderModel() at src/codex/catalog/provider-fetch.ts:1631, which is hardcoded to kimi, xai, and opencode-free — so there is no escape hatch for this provider.

alibaba-token-plan-intl is not in QUIET_AUTHORITATIVE_CATALOG_PROVIDERS, so warnDroppedConfiguredIdsOnce() does emit a one-shot console warning, but GET /api/providers still reports discovery.status = "ok", which is what makes this look like "the model does not exist" from the dashboard.

Suggested fix

Make the fold symmetric and accept MMDD, while keeping it strict enough not to swallow non-date numeric suffixes (e.g. a -4096 context-size suffix):

const DATE_SUFFIX = /^(\d{8}|(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01]))$/;

function hasDatedSuffix(longer: string, shorter: string): boolean {
  return longer.startsWith(`${shorter}-`)
    && DATE_SUFFIX.test(longer.slice(shorter.length + 1));
}

export function isDatedVariantId(liveId: string, configuredId: string): boolean {
  return hasDatedSuffix(liveId, configuredId) || hasDatedSuffix(configuredId, liveId);
}

This stays safe with respect to the concern raised in the #1690 review: the fold still only retains a configured id when a corresponding live row is actually present, and the retained row already goes through applyProviderConfigHints(), so per-model context/reasoning hints from config still win.

Happy to open a PR with unit tests covering both directions and both suffix widths if that is useful.

Redacted configuration

{
  "providers": {
    "alibaba-token-plan-intl": {
      "adapter": "openai-chat",
      "baseUrl": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
      "apiKey": "<redacted>",
      "defaultModel": "qwen3.8-max",
      "liveModels": true,
      "models": [
        "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash",
        "deepseek-v4-pro", "deepseek-v4-pro-0813", "glm-5.2",
        "deepseek-v4-flash-0731", "qwen3.8-flash"
      ]
    }
  }
}

The plan console lists deepseek-v4-pro-0813, deepseek-v4-pro, and deepseek-v4-flash-0731 as three separate entitled Text Generation / Reasoning models, which is why all three are in models.

Related: #1690 (feature request for an opt-in retainModels allow-list — complementary, not the same defect), #1671 (earlier user-facing symptom of a configured model being dropped by live discovery).

Activity

  1. added
    bugSomething isn't working
    catalogModel catalog, slugs, visibility, routed entries
    on Aug 30, 2026
  2. lidge-jun commented on Aug 30, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 66 / 80

    이 이슈가 말하는 구멍은 지금 dev HEAD 870a2adb6 에도 그대로 있다. 패키지 버전은 2.37.0 이다. 작성자가 본 설치본은 2.36.0 이다. 그 사이 올라간 것은 버전 줄(#3013)이지, 날짜 접미사 접기 수리가 아니다. 라이브 디스커버리가 설정에 있는 호출 가능한 모델 id 를 카탈로그에서 조용히 빼는 길이다.

    증상은 이렇다. alibaba-token-plan-intl 설정에 deepseek-v4-pro-0813 이 들어 있다. 업스트림 /models 는 베이스 id deepseek-v4-pro 만 광고한다. 같은 모델은 업스트림에도, 프록시 네임스페이스로도 HTTP 200 으로 호출된다. 그런데 ocx models live 와 GET /api/models 에서는 그 날짜 id 가 사라진다. GET /api/providers 의 discovery.status 는 그냥 ok 다. 대시보드와 Codex picker 는 죽은 모델처럼 그린다. 라우팅 구멍은 아니다. 카탈로그 투영 구멍이다.

    접기 함수가 한쪽으로만 본다. isDatedVariantId(liveId, configuredId) 는 live 가 configured 에 -YYYYMMDD 를 붙인 경우만 참이다. 설정이 날짜 id 이고 라이브가 베이스 id 인 반대 방향은 startsWith 에서 바로 거짓이다. 설정을 라이브에 대입해도 접미사 0813 은 8자리가 아니라서 /^\d{8}$/ 가 거절한다. mergeConfiguredModelsIntoLiveCatalog 는 그 함수가 참일 때만 설정 id 를 라이브 행 위에 남긴다. 실패하면 shouldRetainConfiguredProviderModel 로 넘어가는데, 그 탈출구는 호환 허용 목록과 opencode-free 뿐이다. alibaba Token Plan 은 없다. 그래서 설정 id 는 droppedConfiguredIds 로 떨어진다.

    이건 #1690 이 아니다. #1690 은 업스트림이 진짜로 안 주는 행을 남기는 retainModels 옵션이다. 메인테이너 리뷰가 이미 말했다. 무조건 남기면 죽은 행이 살아난다. 이 이슈는 라이브에 대응 행이 있을 때만 접는 기존 길을 고치자는 쪽이다. 초안 PR #2860 과 #2122 는 #1690 구현이다. 이 구멍의 수리로 합치지 마라. 그 PR 들은 types/config 분할에도 걸릴 수 있다. 미리보기 배포는 계획에 없다. 이 접기만 고치는 열린 PR 은 없다.

    같은 시각의 #3023 과도 다르다. #3023 은 네이티브 GPT-5.6 관리면 캐시다. 이 이슈는 라우트 공급자 라이브 병합이다. 작성자가 같은 kaicot 이라도 한 이슈로 묶지 마라.

    라인 src/codex/catalog/provider-fetch.ts 938 - isDatedVariantId 가 live 가 configured-YYYYMMDD 일 때만 참이다. 4자리 MMDD 도, 설정=날짜/라이브=베이스 반대 방향도 거짓이다.
    라인 src/codex/catalog/provider-fetch.ts 1671 - 병합 루프가 그 함수 한 방향으로만 접는다. 실패하면 설정 id 는 드롭 목록으로 간다.
    라인 src/codex/catalog/provider-fetch.ts 1631 - shouldRetainConfiguredProviderModel 탈출구는 호환 허용 목록과 opencode-free 뿐이다. alibaba-token-plan-intl 은 없다.
    경로 src/codex/catalog/provider-fetch.ts CALLABLE_CONFIGURED_COMPATIBILITY_MODELS - kimi/xai 쪽 별칭 잔류용이다. Token Plan 날짜 id 를 살리는 칸이 아니다.
    경로 GET /api/providers discovery.status - 한 번 경고는 콘솔에 나가도 API 는 ok 라서 대시보드가 드롭을 성공으로 읽는다.

    메인테이너의 판단이 필요한 지점

    너의 추천
    열어 둔다. 라벨을 바꾸지 마라. #1690 중복으로 닫지 마라. #3023 과도 묶지 마라. 초안 #2860 / #2122 를 rebase 해서 이 이슈를 덮지 마라. 고치는 자리는 isDatedVariantId 와 그 한 줄 호출이다. 라이브에 대응 행이 있을 때만 설정 id 를 남기는 조건을 유지하라. 죽은 행을 부활시키지 마라. 좁은 단위 테스트(8자리, 4자리, 반대 방향, -4096 비날짜)를 붙인 작은 PR 로 dev 에 올려라.

    이 댓글은 grok-bot이 작성했습니다

  3. kaicot commented on Aug 30, 2026

    @kaicot
    ContributorAuthor

    Follow-up on my own suggested fix: it is incomplete — please don't merge the regex from the issue body as written. I validated it against real model ids and it only covers a subset of providers.

    I enumerated every numeric-suffixed id pair among the 378 model ids this install exposes via GET /api/models. Caveat on how to read this: the fold only ever compares configured vs live ids within one provider, so these pairs are format-prevalence data, not a list of actual fold candidates. What they do show is which suffix conventions are in use in the wild.

    The current /^\d{8}$/ rule matches 0 of the 26 numeric-suffixed pairs on this install.

    Suffix format Example \d{8} (current) My MMDD proposal Combined
    YYYYMMDD (none observed here) yes yes yes
    YYMMDD solar-pro4-260806, syn-pro-251021 no no yes
    MMDD deepseek-v4-pro-0813, kimi-k2-0905, gpt-3.5-turbo-0613 no yes yes
    YYMM mistral-large-2407, qwen3-235b-a22b-2507 no no yes
    YYYY-MM-DD gpt-4o-2024-08-06, qwen-plus-2025-07-28 no no no
    MM-DD gemini-2.5-pro-preview-05-06 no no no

    So the MMDD-only patch I proposed would fix DeepSeek/Kimi/OpenAI-style ids and leave Mistral, Qwen, and Solar/Syn silently broken.

    Revised numeric rule — 4/6/8 digits, still strict on month/day so version numbers and context sizes are not swallowed:

    const MM = String.raw`(?:0[1-9]|1[0-2])`;
    const DD = String.raw`(?:0[1-9]|[12]\d|3[01])`;
    // YYYYMMDD | YYMMDD | MMDD | YYMM
    const DATE_SUFFIX = new RegExp(
      String.raw`^(?:\d{4}${MM}${DD}|\d\d${MM}${DD}|${MM}${DD}|\d\d${MM})$`,
    );

    4-digit MMDD and YYMM barely overlap in validity (0813 is only valid as MMDD, 2407 only as YYMM), so accepting either is unambiguous enough in practice, and both readings mean "dated release" anyway.

    Against the same 378 ids this rule matches 13 of 26 pairs. The 13 it rejects are all genuinely non-dates or out of scope:

    rejected, correctly:
      5                   mistralai/mistral-medium-3-5          version number
      16k                 openai/gpt-3.5-turbo-16k              context size
      30b-a3b-instruct    qwen/qwen3-coder-30b-a3b-instruct     variant name
      0813:batch          deepseek/deepseek-v4-pro-0813:batch   batch lane of an already-dated id
      0731:batch          deepseek/deepseek-v4-flash-0731:batch
      2512:batch          mistralai/mistral-large-2512:batch
      5:batch             mistralai/mistral-medium-3-5:batch
    
    rejected, but arguably should match (hyphenated ISO -- separate decision for you):
      2024-05-13          openai/gpt-4o-2024-05-13
      2024-08-06          openai/gpt-4o-2024-08-06
      2024-11-20          openai/gpt-4o-2024-11-20
      2024-07-18          openai/gpt-4o-mini-2024-07-18
      2025-07-28          qwen/qwen-plus-2025-07-28
      05-06               google/gemini-2.5-pro-preview-05-06
    

    One caveat I should state plainly. The bidirectional half of my proposal has exactly the downside the #1690 review warned about. With configured = dated, live = base, a deprecated snapshot left in someone's config (say mistral-large-2407 after upstream retires it) would now be retained instead of dropped. For the model in this issue I verified callability directly (the two HTTP 200s in the issue body), but the fold cannot check that in general.

    If that trade-off is not acceptable, there is a narrower scope: keep the fold one-directional and only widen the suffix format. That does not fix deepseek-v4-pro-0813 — the case that opened this issue — but it does fix the base -> dated direction for Mistral, Qwen, Solar and Syn, which is currently dead for every format observed here.

    Happy to send a PR against dev with table-driven tests over all six formats above. Just tell me which scope you want:

    1. widen suffix format only (one-directional, no resurrection risk), or
    2. widen format + make the fold bidirectional (fixes this issue, accepts the deprecated-snapshot risk).
  4. lidge-jun commented on Aug 31, 2026

    @lidge-jun
    Owner

    Thank you for withdrawing your own patch with data instead of letting it get merged. That saved a bad fold from shipping, and the format table is the most useful thing in this thread.

    I verified your revised regex against the values in your table plus the ones I care about, and I am picking scope 1: widen the suffix format, keep the fold one-directional — with one change to your pattern.

    Where your revised rule needs an edit. MMDD and YYMM both being 4 digits is worse than "barely overlap". Two concrete collisions:

    1024   → matches (reads as Oct 24)
    1301   → matches (reads as YYMM: year 13, month 01)
    

    1024 is the one that matters. Context sizes appear as bare numeric suffixes in real ids, -1024 is a plausible one, and no month/day tightening can exclude it because October 24th is a real date. 1301 is excludable — restricting YY to 2\d drops it while keeping every observed id (2407, 2507, 2512, 260806, 251021):

    const YY = String.raw`(?:2\d)`;   // 2020s, not any two digits
    const DATE_SUFFIX = new RegExp(
      String.raw`^(?:\d{4}${MM}${DD}|${YY}${MM}${DD}|${MM}${DD}|${YY}${MM})$`,
    );

    I checked 2048, 4096, 8192 are all rejected by both versions; only 1024 survives, and that one is irreducible. I would rather accept a rare false positive on a 4-digit context suffix than leave Mistral/Qwen/Solar/Syn dead, but it should be a known cost written into the test table, not a surprise later.

    Why one-directional. The resurrection risk you named is the deciding factor. isDatedVariantId exists so a configured base id can inherit a live dated row's metadata — the configured id is what the operator asked for and what gets served. Reversing it means a configured id the provider no longer lists gets retained on the strength of a format match, and the fold has no callability signal to check that with. You verified deepseek-v4-pro-0813 by hand with two 200s; the code cannot. #1690 is the right place for "operator insists on keeping this id" because it is an explicit allow-list, not an inference.

    So deepseek-v4-pro-0813 stays unfixed by this change, and that is a deliberate call rather than an oversight. The honest framing: your issue found two defects, and I am fixing the one that can be fixed safely.

    PR scope, if you still want to send it. Please do:

    • isDatedVariantId at src/codex/catalog/provider-fetch.ts:938 — widen the numeric suffix, keep direction configured=base → live=dated
    • table-driven tests over all six formats in your table, including the rejections, and an explicit case documenting that 1024 is accepted as MMDD
    • extend the existing case at tests/codex-catalog.test.ts:3504 rather than adding a parallel block
    • no change to shouldRetainConfiguredProviderModel or the retainModels path

    Target dev. Hyphenated ISO (gpt-4o-2024-08-06, gemini-2.5-pro-preview-05-06) is out of scope here — a hyphenated suffix is ambiguous against ordinary name segments and needs its own decision.

    Priority stays 66/80. Still open, now with an agreed shape.

  5. lidge-jun commented on Sep 1, 2026

    @lidge-jun
    Owner

    Landed on dev via #3100 at b46164e. Exact PR head checks were green and the landed commit was verified in the current dev history. Closing this bug as fixed.

  6. github-actions commented on Sep 1, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

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

    bugSomething isn't workingcatalogModel catalog, slugs, visibility, routed entries

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions