Repository navigation
[Bug] selectedModels, catalog sync, CLI, and routing use four different slug-equivalence relations #2491
Description
Client or integration
Direct HTTP/API client
Area
Catalog / models
Summary
selectedModels matching, sync.ts merge, CLI removal, and routing each decide "does this stored slug name this model?" with a different primitive, and the relations disagree in exactly one case: a provider that publishes two ids whose Codex-facing encoded forms are identical, such as a/b and a-b.
filterCatalogVisibleModelsandmergeCatalogEntriesFromObservedStateuseslugEquivalenceKey(routedSlug(...)), which is deliberately lossy — the two ids collapse to one key, so selecting either one makes both visible.decodeRoutedModelIdOrThrow, used by routing, refuses to guess and throwsambiguous model id.ocx models removeusesslugEquals, a third relation.
So the catalog can present a pair that the router will not resolve, and an allowlist can grant a model the operator never selected. Both are narrow, but they are silent.
This is deliberately not fixed in #2481, which is a bugfix-only hotfix for the far more common case (a slash-bearing id selected by the encoded slug the picker displays was hidden entirely). The lossy behavior is pinned by tests in tests/selected-models.test.ts under "a provider publishing both spellings (known lossy case)" so it is recorded rather than undiscovered.
One approach was tried and rejected, and it is worth recording so it is not re-attempted: resolving each selectedModels entry against the provider's current rows with decodeRoutedModelId. That roster is an incomplete dictionary — live discovery can omit a published id — so an exact a-b selection still resolves onto a/b whenever a-b is missing from that snapshot, reproducing the same over-grant. It would also make fresh filtering disagree with persisted merge, which is the bug class the change was removing.
A real fix needs one selection resolver, shared by filtering, persisted sync, CLI, and routing, evaluated against a complete known-id set (config ∪ registry ∪ live cache ∪ custom), with one explicit ambiguity policy. That is an architecture change, not a hotfix.
Reproduction
- Configure a provider that publishes both
a/banda-b(any OpenAI-compatible custom provider, or a live-discovery provider such as Together or Fireworks that returns both spellings). - Set
selectedModels: ["a-b"]for that provider. - Request
/v1/models.
Observed: both a/b and a-b are listed.
Expected: only a-b.
Then request the model <provider>/a-b through the router: it throws ambiguous model id rather than resolving, so the catalog offered something routing declines.
Version
2.32.0 (dev at 3e3a028)
Operating system
macOS
Provider and model
Any provider publishing both a/b and a-b; reproduced against the catalog filter directly rather than a live vendor. A survey of current public rosters (OpenRouter 416 ids, NVIDIA 102, ZenMux 166) found no live collision today, so this is a latent invariant rather than an active outage.
Logs or error output
selectedModels: ["a-b"] rows: ["a/b", "a-b"]
filterCatalogVisibleModels -> ["a/b", "a-b"] # both, only one selected
decodeRoutedModelIdOrThrow("a-b", ["a/b","a-b"]) -> throws: ambiguous model id "a-b"
Redacted configuration
{ "providers": { "p": { "selectedModels": ["a-b"] } } }Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
test(catalog): pin the lossy collision and record the rejected altern…
fix(catalog): match selectedModels the way the canonical resolver mat…
lidge-jun commentedon Aug 24, 2026
리뷰 · 우선순위 67 / 80
설명: 이 이슈는 저장소 주인이 연 설계 버그다. selectedModels 가 보이는지, sync.ts 가 합치는지, CLI 가 지우는지, 라우터가 풀는지는 서로 다른 동등 관계로 판단한다. 네 관계가 어긋나는 한 가지 경우가 있다. 제공자가 a/b 와 a-b 처럼 인코딩하면 같은 문자열이 되는 두 아이디를 함께 공개하는 경우다. 지금 개발 가지 a60d517 에는 2481 이 이미 착지해 있다. 2481 은 고른 인코딩 슬러그가 목록에서 사라지던 흔한 구멍을 막은 핫픽스다. 이 이슈 본문도 2481 이 네 관계의 불일치를 고치지 않는다고 분명히 적었다. 그래서 2481 착지만으로 이 이슈를 닫으면 안 된다. default-aliases.ts 와 model-presets.ts 는 여전히 없다. types.ts 와 config.ts 가르기와는 무관하다. 프리뷰 배포도 아니다.
지금 HEAD 를 읽으면 네 길이 그대로다. src/codex/catalog/provider-fetch.ts 의 filterCatalogVisibleModels 는 허용 집합을 slugEquivalenceKey(routedSlug(name, model)) 로 만든다. 이 키는 일부러 손실이 있다. a/b 와 a-b 가 한 키로 접혀서, 하나만 골라도 둘 다 보인다. src/codex/catalog/sync.ts 의 합치기도 같은 키를 쓴다. src/providers/slug-codec.ts 의 decodeRoutedModelIdOrThrow 는 추측하지 않고 ambiguous model id 를 던진다. src/cli/models.ts 의 삭제는 slugEquals 를 쓴다. 카탈로그는 라우터가 거부할 쌍을 보여줄 수 있고, 허용 목록은 운영자가 고르지 않은 모델을 줄 수 있다. 본문이 말하듯 지금은 공개 카탈로그에서 충돌이 없다. 잠복 불변식이다. tests/selected-models.test.ts 가 그 손실 경우를 고정해 두었다.
이미 한 번 시도했다가 버린 길이 있다. selectedModels 각 항목을 지금 스냅샷의 decodeRoutedModelId 로 푸는 것이다. 그 명부는 불완전해서, 스냅샷에 a-b 가 없으면 고른 a-b 가 a/b 로 붙는다. 같은 과잉 허용이 다시 나온다. 신선한 필터와 저장된 합치기도 갈라진다. 진짜 고침은 필터, 저장 합치기, CLI, 라우터가 하나의 선택 해석기를 공유하고, 완전한 알려진 아이디 집합(config ∪ registry ∪ live cache ∪ custom) 위에서 모호함을 한 정책으로 다루는 것이다. 핫픽스가 아니라 구조 변경이다.
src/codex/catalog/provider-fetch.ts filterCatalogVisibleModels - slugEquivalenceKey(routedSlug(...)) 허용 집합이라 a/b 와 a-b 가 함께 보인다
src/codex/catalog/sync.ts - 같은 손실 키로 합친다. 필터와 저장이 이 키로는 같지만 라우터와는 다르다
src/providers/slug-codec.ts decodeRoutedModelIdOrThrow - 모호하면 던진다. 카탈로그가 보여 준 아이디를 라우터가 거절한다
src/cli/models.ts slugEquals - 세 번째 관계다. 삭제와 표시가 어긋날 수 있다
tests/selected-models.test.ts - 두 철자를 공개하는 제공자의 손실 경우를 고정한다. 2481 이 이 시험을 깨지 않은 것이 의도다
2481 - 인코딩 슬러그로 고른 모델이 숨던 흔한 구멍만 막았다. 이 이슈의 네 관계 불일치는 남아 있다. 닫지 말 것
메인테이너의 판단이 필요한 지점
너의 추천
이 이슈는 연다. 2481 로 닫지 말 것. 하나의 선택 해석기가 착지하기 전에는 닫지 않는다. 지금 합칠 풀도 아니다. 호출 길을 넓히지 말 것. 프리뷰 배포가 아니다. 2463 과 2464 과 2465 도 구현 파일이 없어 그대로 연다.
이 댓글은 grok-bot이 작성했습니다
feat(slug): one selection resolver that reports what it matched (#2594)
fix(cli): route models remove through the shared slug relation (#2596)
lidge-jun commentedon Aug 25, 2026
Fixed on
devacross #2594 and #2596.Checking the four relations against the tree, two of them — catalog visibility and persisted sync — already shared
slugEquivalenceKey, so they agreed with each other. The real divergence wasocx models remove:slugEqualscompares the raw and encoded spellings of ONE id, so a native-slash selector matched only the slash row while the encoded form matched both.#2594 added
resolveSlugSelection, which keeps the tolerant matching and additionally reports the exact id and whether the selection was ambiguous — writing its test caught a slash-qualification asymmetry in my own first version. #2596 routes removal through it, so all three surfaces now see the same collision class.Two deliberate non-changes. Ambiguous selectors still refuse: that is correct for a destructive command, and the widening means it now refuses in a case where it previously deleted a row silently. And
decodeRoutedModelIdOrThrowkeeps its own encode-collision check, because it answers a different question (may this request route at all) and already fails closed — folding it in would trade a hard refusal for a softer match on the request path.The lossy encoding itself stays: Codex's models-manager resolves metadata with an exact one-slash rule, so
a/banda-bmust share an encoded form. What is fixed is that every surface now agrees about it.Merge from upstream (#8)
fix(catalog): match selectedModels the way the canonical resolver mat…
feat(slug): one selection resolver that reports what it matched (lidg…
fix(cli): route models remove through the shared slug relation (lidge…
fix(catalog): match selectedModels the way the canonical resolver mat…
feat(slug): one selection resolver that reports what it matched (lidg…
fix(cli): route models remove through the shared slug relation (lidge…