Skip to content

[Bug] selectedModels, catalog sync, CLI, and routing use four different slug-equivalence relations #2491

Closed

Description

@lidge-jun

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.

  • filterCatalogVisibleModels and mergeCatalogEntriesFromObservedState use slugEquivalenceKey(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 throws ambiguous model id.
  • ocx models remove uses slugEquals, 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

  1. Configure a provider that publishes both a/b and a-b (any OpenAI-compatible custom provider, or a live-discovery provider such as Together or Fireworks that returns both spellings).
  2. Set selectedModels: ["a-b"] for that provider.
  3. 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

  1. added
    catalogModel catalog, slugs, visibility, routed entries
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Aug 24, 2026
  2. lidge-jun commented on Aug 24, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 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 - 인코딩 슬러그로 고른 모델이 숨던 흔한 구멍만 막았다. 이 이슈의 네 관계 불일치는 남아 있다. 닫지 말 것

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

    • 완전한 알려진 아이디 집합을 어디서 만들지. config, 레지스트리, 라이브 캐시, custom 을 한 집합으로 모을지
    • 모호할 때 거절할지, 운영자에게 고르게 할지. 라우터는 지금 거절한다
    • 2481 핫픽스를 유지한 채 이 이슈만 구조 작업으로 둘지. 그렇게 하는 편이 맞다
    • 공개 카탈로그에 충돌이 없다고 미룰지. 잠복 불변식이므로 열어 두는 편이 맞다

    너의 추천
    이 이슈는 연다. 2481 로 닫지 말 것. 하나의 선택 해석기가 착지하기 전에는 닫지 않는다. 지금 합칠 풀도 아니다. 호출 길을 넓히지 말 것. 프리뷰 배포가 아니다. 2463 과 2464 과 2465 도 구현 파일이 없어 그대로 연다.

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

  3. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    OwnerAuthor

    Fixed on dev across #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 was ocx models remove: slugEquals compares the raw and encoded spellings of ONE id, so a native-slash selector matched only the slash row while the encoded form matched both.

    p/a-b | slugEquals -> [a/b, a-b] | resolver -> [a/b, a-b]
    p/a/b | slugEquals -> [a/b]      | resolver -> [a/b, a-b]
    

    #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 decodeRoutedModelIdOrThrow keeps 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/b and a-b must share an encoded form. What is fixed is that every surface now agrees about it.

  4. added a commit that references this issue on Aug 26, 2026
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 entriesproviderProvider adapters, OpenAI-compat presets, upstream API quirks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions