Skip to content

fix(api): stop leaking the internal provider UUID in /v1/models and honor the configured prefix (#8327) - #8361

Merged
diegosouzapw merged 1 commit into
release/v3.8.49from
fix/8327-models-owned-by-prefix
Jul 24, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.49from
fix/8327-models-owned-by-prefix

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Refs #8327

Root cause

src/app/api/v1/models/catalog.ts builds two different identifiers per model
entry for compatible-provider nodes (openai-compatible / anthropic-compatible):

  • alias — DOES receive providerIdToPrefix (built from provider nodes) and
    correctly becomes the operator's configured prefix for the published id field.
  • canonicalProviderId (via resolveCanonicalProviderId()) — used for owned_by,
    but only knows the static AI_PROVIDERS/PROVIDER_MODELS alias maps. A
    compatible-provider node's id is an internal UUID (e.g.
    openai-compatible-chat-<uuid>, per createProviderNode()'s
    id: data.id || uuidv4()) that is never present in those static maps, so it
    falls through every lookup and returns the raw UUID verbatim — leaking the
    internal provider-node id into the public owned_by field for every emit
    site that uses it: the synced-models block (the one matching the reporter's
    scenario), the custom-models block, the model-alias block, and the
    managed-fallback block (which additionally skipped canonicalProviderId
    resolution entirely, using the raw providerId directly).

Fix

canonicalProviderId is still required, unmodified, by internal
connection/hidden-model/registry lookups that are keyed on the raw provider-node
id (getConnectionsForProvider, getModelIsHidden, etc.) — so I did not change
what it resolves to. Instead I added a dedicated resolvePublicOwnerId(providerId, canonicalProviderId) helper that checks providerIdToPrefix first and applied
it at every owned_by emit site (previously owned_by: canonicalProviderId /
owned_by: providerId). Built-in providers are unaffected — providerIdToPrefix
only has entries for actual provider nodes with a configured prefix, never for
static AI_PROVIDERS ids.

Scope

This PR fixes symptom (a) — the internal provider UUID leaking as owned_by —
and symptom (b) — the configured prefix being ignored for owned_by. It does
not touch effort-variant multiplication, which the triage plan confirmed is
intentional, documented design (open-sse/utils/claudeEffortVariants.ts,
open-sse/utils/syncedEffortVariants.ts), not a defect. Using Refs #8327
rather than Closes #8327 since the issue as filed also raises that symptom.

Regression test (Hard Rule #18 — TDD)

New file tests/unit/8327-models-owned-by-prefix.test.ts, reusing the triage
plan's proven repro scenario (compatible provider node + configured prefix +
synced/custom models).

  • RED confirmed against the pre-fix code (temporarily reverted
    catalog.ts via git checkout HEAD -- <file> in-worktree, no stash used):
    AssertionError [ERR_ASSERTION]: owned_by must be the configured prefix
    "pix4k-talk", not the raw provider-node id — got
    "openai-compatible-chat-550e8400-e29b-41d4-a716-446655440000"
    
  • GREEN after restoring the fix: all 3 new tests pass (synced models, custom
    models, and a built-in-provider contract-preservation check).

Gates run (all green)

  • node scripts/check/check-file-size.mjs — OK
  • node scripts/check/check-complexity.mjs — pre-existing baseline drift
    (2167 violations), confirmed identical on a disposable probe worktree pinned
    to origin/release/v3.8.49 — not introduced by this diff.
  • node scripts/check/check-cognitive-complexity.mjs — same pre-existing
    drift (956), confirmed identical on the same probe — not introduced by this diff.
  • npm run typecheck:core — clean
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files> — 0 errors
  • Sibling tests of the touched area (26 files covering api/v1/models/catalog) — all green, including models-catalog-route.test.ts (43 tests)
  • node scripts/check/check-changelog-integrity.mjs — OK

Changelog

changelog.d/fixes/8327-models-owned-by-prefix.md

@diegosouzapw
diegosouzapw merged commit 9b7bd6e into release/v3.8.49 Jul 24, 2026
10 checks passed
@diegosouzapw
diegosouzapw deleted the fix/8327-models-owned-by-prefix branch July 24, 2026 15:36
korvin2000 pushed a commit to korvin2000/OmniRoute that referenced this pull request Aug 2, 2026
GET /v1/models was assembled by many independent push loops (auto-combos,
named combos, static registry, codex-native, synced, OpenRouter, specialty,
custom, alias-backed, connection-fallback), so one provider's models landed in
several separated, interleaved blocks. Apply ONE stable, provider-grouped sort
at serialization in finalizeCatalogResponse, keyed by owned_by (canonical owner
identity) rather than the model-id prefix — so a single routable public prefix
that differs from its owner (e.g. no-auth OpenCode publishing oc/<model> while
keeping owned_by "opencode") stays contiguous.

Combos are pinned first (preserving diegosouzapw#4164); then providers in registry
precedence (OAuth -> NoAuth -> API-key); then unknown providers in
locale-independent code-unit order. The sort is stable and pure (reorders rows
only, no mutation, no DB/IO), preserving combo sort_order, connection priority,
custom append-order, and equal-id audio twins. Identity, alias mapping, effort
variants, dedupe, and Claude-mirror gating are untouched.

This is the one enumerated Model-Identity behavior with no upstream equivalent
on release/v3.8.50: single public prefix / UUID-leak (diegosouzapw#8327/diegosouzapw#8361) and Combo
Builder effort variants (diegosouzapw#8072/diegosouzapw#8165) are already merged there.
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant