Repository navigation
[Bug][2.36.0] Live discovery drops a callable configured model: isDatedVariantId only matches YYYYMMDD and only folds base->dated #3024
Description
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:
- It only accepts an 8-digit
YYYYMMDDsuffix. Providers that use a 4-digitMMDDsuffix (Alibaba Token Plan / DeepSeek:deepseek-v4-pro-0813,deepseek-v4-flash-0731) never match. - 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
-
Configure
alibaba-token-plan-intl(international Token Plan,ap-southeast-1) withliveModels: trueand amodelslist that matches the plan's text/reasoning models, includingdeepseek-v4-pro-0813. -
Confirm the configured list is intact:
GET /api/providers -> models: [ ..., "deepseek-v4-pro", "deepseek-v4-pro-0813", ... ] (9 ids) -
Run live discovery:
ocx models live --provider alibaba-token-plan-intl -
Observe that
deepseek-v4-pro-0813is gone, whiledeepseek-v4-proremains.GET /api/modelsshows the same 10 rows. -
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-")isfalse-> 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
lidge-jun commentedon Aug 30, 2026
리뷰 · 우선순위 66 / 80
이 이슈가 말하는 구멍은 지금
devHEAD870a2adb6에도 그대로 있다. 패키지 버전은2.37.0이다. 작성자가 본 설치본은2.36.0이다. 그 사이 올라간 것은 버전 줄(#3013)이지, 날짜 접미사 접기 수리가 아니다. 라이브 디스커버리가 설정에 있는 호출 가능한 모델 id 를 카탈로그에서 조용히 빼는 길이다.증상은 이렇다.
alibaba-token-plan-intl설정에deepseek-v4-pro-0813이 들어 있다. 업스트림/models는 베이스 iddeepseek-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 라서 대시보드가 드롭을 성공으로 읽는다.
메인테이너의 판단이 필요한 지점
-4096같은 숫자 접미사를 날짜로 오인하지 않게 달력 가드가 필요하다discovery.status를 드롭이 있으면 실패/부분으로 바꿀지, 콘솔 경고만 둘지너의 추천
열어 둔다. 라벨을 바꾸지 마라. #1690 중복으로 닫지 마라. #3023 과도 묶지 마라. 초안 #2860 / #2122 를 rebase 해서 이 이슈를 덮지 마라. 고치는 자리는
isDatedVariantId와 그 한 줄 호출이다. 라이브에 대응 행이 있을 때만 설정 id 를 남기는 조건을 유지하라. 죽은 행을 부활시키지 마라. 좁은 단위 테스트(8자리, 4자리, 반대 방향,-4096비날짜)를 붙인 작은 PR 로dev에 올려라.이 댓글은 grok-bot이 작성했습니다
kaicot commentedon Aug 30, 2026
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.\d{8}(current)YYYYMMDDYYMMDDsolar-pro4-260806,syn-pro-251021MMDDdeepseek-v4-pro-0813,kimi-k2-0905,gpt-3.5-turbo-0613YYMMmistral-large-2407,qwen3-235b-a22b-2507YYYY-MM-DDgpt-4o-2024-08-06,qwen-plus-2025-07-28MM-DDgemini-2.5-pro-preview-05-06So 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:
4-digit
MMDDandYYMMbarely overlap in validity (0813is only valid as MMDD,2407only 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:
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 (saymistral-large-2407after 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 thebase -> dateddirection for Mistral, Qwen, Solar and Syn, which is currently dead for every format observed here.Happy to send a PR against
devwith table-driven tests over all six formats above. Just tell me which scope you want:lidge-jun commentedon Aug 31, 2026
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.
MMDDandYYMMboth being 4 digits is worse than "barely overlap". Two concrete collisions:1024is the one that matters. Context sizes appear as bare numeric suffixes in real ids,-1024is a plausible one, and no month/day tightening can exclude it because October 24th is a real date.1301is excludable — restrictingYYto2\ddrops it while keeping every observed id (2407,2507,2512,260806,251021):I checked
2048,4096,8192are all rejected by both versions; only1024survives, 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.
isDatedVariantIdexists 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 verifieddeepseek-v4-pro-0813by 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-0813stays 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:
isDatedVariantIdatsrc/codex/catalog/provider-fetch.ts:938— widen the numeric suffix, keep directionconfigured=base → live=dated1024is accepted as MMDDtests/codex-catalog.test.ts:3504rather than adding a parallel blockshouldRetainConfiguredProviderModelor theretainModelspathTarget
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.
fix(catalog): fold dated model aliases in both directions and accept …
fix(catalog): fold dated model aliases in both directions and accept …
fix(catalog): keep the dated fold directional, per review
fix(catalog): recognize YYMMDD/MMDD/YYMM dated model id suffixes (lid…
fix(catalog): recognize YYMMDD/MMDD/YYMM dated model id suffixes (#3024)
test(catalog): guard the dated fold's direction at the merge loop, no…
fix(catalog): widen the dated-variant fold and guard its direction at…
lidge-jun commentedon Sep 1, 2026
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.
github-actions commentedon Sep 1, 2026
Automated translation bookkeeping — detected language: English.
fix(catalog): widen the dated-variant fold and guard its direction at…
fix(providers): refresh the Alibaba Token Plan catalogs against the l…
fix(providers): refresh the Alibaba Token Plan catalogs against the l…
fix(providers): refresh the Alibaba Token Plan catalogs against the l…
fix(providers): refresh the Alibaba Token Plan catalogs against the l…
fix(providers): refresh the Alibaba Token Plan catalogs against the l…
fix(catalog): widen the dated-variant fold and guard its direction at…