-
[OPIK-5021] [BE][FE] feat: fetch LLM model list from backend registry with CDN-driven labels (#6410)
发布于
2026-04-27 10:39:27 +00:00 - [OPIK-5021] [BE] [FE] [INFRA] feat: fetch LLM model list from backend registry with CDN-driven labels
Replace the ~2000-line static PROVIDER_MODELS constant in the frontend with
a React Query fetch from GET /v1/private/llm/models. Extend the backend YAML
schema with alabelfield so human-readable model names ("Claude Opus 4.7")
flow from the CDN-synced YAML into the dropdown without a frontend deploy.
Plumb the existing LLM_MODEL_REGISTRY_* env vars through docker-compose and
Helm so self-hosted customers can point at their own CDN or mount an override
YAML.Backend
- Add nullable
labelfield to LlmModelDefinition record; JsonInclude(NON_NULL)
keeps the JSON response shape unchanged for records without labels. - Guard LlmModelRegistryService.load() so a malformed customer override YAML
no longer takes down routing; logs the error and falls back to classpath +
optional remote. - Regenerate llm-models-default.yaml via the daily sync script so every
managed-provider entry carries a curated label and sits in the
dropdown-sorted order (GPT 5.4 → 5.2 → 5.1; Opus → Sonnet → Haiku
within each Anthropic generation).
Frontend
- New useLlmModels React Query hook (retry: 3 locally overriding the
global retry: false) fetches the registry and caches it. - Rewrite useLLMProviderModelsData to merge the fetched registry with the
existing useOpenAICompatibleModels output and a minimal Opik Free fallback.
Export getLatestProviderModelsSnapshot and getLatestModelFlags as
module-level readers so pure utility functions can consult the latest
data without hook plumbing. - Wire isReasoningModel to consult the fetched
reasoningflag first,
falling back to the hardcoded REASONING_MODELS list pre-hydration. - Migrate lib/provider.ts and v1/v2 OptimizationModelSelect off the deleted
PROVIDER_MODELS constant.
Sync script
- Feed the dropdown-filtered + curated-sorted entries to the YAML writer
(matching the TS PROVIDER_MODELS ordering the FE previously shipped). - Emit
label: ...only when it differs from the id, keeping the YAML
compact for OpenRouter.
Deployment
- docker-compose and Helm values.yaml expose the five LLM_MODEL_REGISTRY_*
env vars with defaults mirroring apps/opik-backend/config.yml. Without
this plumbing, operators setting these on the host could not reach the
backend container (same class of fix as commit727fe1e8c2for
TOGGLE_FORCE_WORKSPACE_VERSION).
Documentation
- New self-host configure page walks through the three-tier registry
(classpath / CDN / override), the YAML schema, merge semantics, and
the Docker Compose and Helm mounting patterns for supplying private
models.
Fallback chain covered end-to-end:
classpath defaults (shipped in jar)
→ merged with remote CDN YAML (if remoteEnabled)
→ merged with local override YAML (if configured)
→ served to FE at GET /v1/private/llm/models
→ merged with useOpenAICompatibleModels
→ merged with MINIMAL_FALLBACK (Opik Free)Smoke-tested locally: classpath labels render correctly, a local override
YAML adds a model at the end of its provider list, and an HTTP server
playing the role of the CDN supplies additional models that merge in
between classpath and override. v1 and v2 UIs share the hook and render
identically.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(llm-registry): address review feedback on merge-key docs and snapshot key validation
- Clarify in self-host docs that override dedup is always keyed by
id
across every provider;qualifiedNameparticipates in routing lookups
only. The prior phrasing "(or qualifiedName for Vertex AI)" was wrong —
LlmModelRegistryService.merge()always dedupes bym.id(). - Guard
getProviderFromModelagainst snapshot keys that don't match a
knownPROVIDER_TYPE(e.g. a future CDN-added provider key that the FE
enum doesn't know yet). Downstream reads likePROVIDERS[providerType]
would otherwise silently fall through.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(llm-registry): memoize providerModels, seed store from static constants, split store into own module
Addresses PR review feedback from @awkoy on memoization regression,
hydration-window correctness, back-channel state hygiene, and a quiet
circular import.-
Memoization.
getProviderModels()returned a fresh object each call,
so every consumer that putproviderModelsin auseMemodep
invalidated the memo every render. Now the hook builds the map via
useMemoonce and exposes it asproviderModels;getProviderModels
stays as a compat wrapper returning the same memoized reference.
Hot consumers (useModelOptions,OptimizationModelSelect,
usePromptModelDisplay) now take / readproviderModelsdirectly,
so filter/search pipelines no longer rebuild on every keystroke. -
Hydration-window correctness. The module-level snapshot/flag cells
started as{}/ empty Map. Cold-load reads of
getProviderFromModel/isReasoningModelreturned OPEN_AI / false
for every non-OpenAI / reasoning model until the fetch resolved
(~200-500ms). Now seeded from the static PROVIDER_MODELS and
REASONING_MODELS constants, so pre-fetch reads return the right
answers for every model known at release time; the fetch still
overwrites on mount with CDN-fresh data. -
Store ownership. Module-level mutable state is now in a dedicated
lib/modelRegistryStore.tswith an explicitresetForTesting()
export. Makes the bridge between React Query data and pure utility
functions visible, testable, and cycle-free. -
Circular import.
hooks/useLLMProviderModelsData.tsimports
parseComposedProviderTypefromlib/provider.ts;
lib/provider.tspreviously importedgetLatestProviderModelsSnapshot
back from the hook. Broken by routing the getter through the new
store module — neither side depends on the other at module eval. -
Expose query state. The hook now returns
isPending,isError,
errorfrom the underlyinguseLlmModelsquery so picker
components can render skeletons instead of empty lists during the
hydration window. Consumers can opt in incrementally. -
PROVIDER_MODELS moved to
constants/providerModels.ts(kept as
dead-code rollback handle per OPIK-5022). The hook file re-exports
it so existing imports keep working.
Typecheck + lint + build all green.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(llm-registry): filter non-dropdown entries, preserve seeded snapshot, guard composed provider keys
Three issues found after round 3:
-
Dropdown was leaking non-curated entries. The YAML superset fix from
an earlier commit meanttransformFetchedwas feeding dated
snapshots (e.g.claude-opus-4-6-20260205) into the dropdown map
alongside curated entries. The sync script's convention is that
curated entries carry alabeland non-curated don't — use that as
the filter signal. The store still receives the FULL registry (so
getProviderFromModelresolves every routable id), the dropdown
only gets labelled entries. -
setLatestProviderModelsSnapshotwas clobbering the seeded catalog.
The effect ran on first mount withfetched=undefinedand wrote
{...MINIMAL_FALLBACK, ...openAICompatibleModels}— an empty-ish
map — over the PROVIDER_MODELS-seeded snapshot from
modelRegistryStore.ts init. Result: every persisted non-OpenAI
prompt briefly resolved as OpenAI during the hydration window,
exactly the bug the seed was supposed to prevent. Fix: when
fetchedis still undefined, merge onto the existing seeded
snapshot instead of replacing it. -
calculateModelProviderdidn't guard against unknown snapshot
keys. Added the same KNOWN_PROVIDER_TYPES checkgetProviderFromModel
uses, viaparseComposedProviderTypeso composed keys like
custom-llm:acmestill pass (their base type is a known PROVIDER_TYPE).
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- refactor(llm-registry): keep PROVIDER_MODELS in its original module
Move PROVIDER_MODELS back into useLLMProviderModelsData.ts and remove
the short-lived constants/providerModels.ts split.Rationale: the physical extraction was a cleanliness choice, not a
requirement for any of @awkoy's review comments. The store
(lib/modelRegistryStore.ts) imports PROVIDER_MODELS from the hook file
again, which does form a cycle — but it's a pure-data cycle (no function
calls at module eval time), strictly safer than the hook ↔ lib/provider.ts
function-level cycle that was the original Y5 concern. Verified OK by
Vite prod build + full unit test suite (1408 tests).No behavioural change from the previous commit. Same final state as
round 3 just without the file move, so the GitHub stat line drops
from +2977/-2205 to what the actual behavioural diff deserves.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(llm-registry): resolve persisted model ids against full registry; regen helm docs
-
calculateModelProvidernow consults a full, unfiltered
fullProviderModelsmemo for resolution. The dropdown-facing
providerModelsis still filtered to labelled entries, but provider
resolution must cover persisted selections that point at non-curated
ids (dated snapshots, historically-selectable models that were later
dropped from the dropdown). Without this, a saved Claude prompt with
claude-opus-4-6-20260205resolves to "" and temperature gating /
provider-key lookup silently fail. -
Match persisted bare ids against qualified-name values so a saved
gemini-2.5-proselection still resolves when the Vertex AI registry
entry carriesvertex_ai/gemini-2.5-pro. Covers the persistence
compatibility gap betweenm.idandm.qualifiedName. -
Move
lodash/firstabove the@/-prefixed imports to match the
project's React → external → internal import ordering. -
Regenerate
deployment/helm_chart/opik/README.md— version bump from
the latest main merge (2.0.6 → 2.0.11) triggered the update-readme
CI check.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(llm-registry): CDN opt-in, two-pass model resolution, break dep-cruiser cycle
Three fixes landed together since they touched overlapping files.
Revert CDN defaults to opt-in (@andrescrz review):
config.yml:remoteEnabled=false,remoteUrlempty- docker-compose + Helm values: mirror the backend default
- Both self-host docs rewrite: "CDN is opt-in" is the framing; keep
the "enable the remote CDN fetch" section for operators who want it
Self-hosted / OSS deployments make no outbound calls by default and
rely on the classpath YAML shipped with the release. Comet SaaS enables
the remote path via its own Helm overrides.
Two-pass model resolution in
calculateModelProvider(baz-reviewer):
Pass 1 does exactm.valuematch across every provider, so a stored
vertex_ai/gemini-2.5-pronever gets captured by the Gemini direct
entry. Pass 2 is the bare-id suffix fallback only when no exact match
exists, for persisted selections stored without the qualifier prefix.
Fixes the suffix-first ordering ambiguity the bot flagged on
gemini-2.5-pro/vertex_ai/gemini-2.5-pro.Break hook ↔ store circular import (dep-cruiser CI):
Extracted PROVIDER_MODELS intosrc/constants/providerModels.ts.
lib/modelRegistryStore.tsimports from there instead of the hook,
resolving the fourno-circularviolations CI flagged. The hook
re-exports PROVIDER_MODELS for backward compat with any stale
imports.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- fix(llm-registry): recover from merge-conflict regressions
Three fixes for issues introduced by the main-merge resolution:
-
useLLMProviderModelsData.ts was left in a broken state: the merge
combined both sides' import blocks (producing a syntactically invalid
interleaving of twoimport {...} from "@/types/providers"statements)
and re-inserted the full ~2,000-line PROVIDER_MODELS constant that
this branch had extracted into src/constants/providerModels.ts. Result:
parsing error at line 6, FE lint CI failure, and the hook ↔ store
circular import regression. Restored the hook file to its pre-merge
state from ac8d7360c0; main has not touched this file since the
branch diverged so a clean overwrite is safe. -
llm-models-default.yaml lost
reasoning: trueon eight dated OpenAI
snapshots during the conflict resolution (o1-2024-12-17, o1-mini,
o1-mini-2024-09-12, o1-preview, o1-preview-2024-09-12, o1-pro,
o3-deep-research, o3-pro). Without the flag, selecting any of these
models in a persisted prompt would compute temperature=0 and get
rejected by the OpenAI API. Restored. -
deployment/helm_chart/opik/README.md was out of date vs Chart.yaml
(2.0.11 -> 2.0.12) after the main merge brought in the version bump.
Regenerated viapre-commit run helm-docs --all-files.
- chore(helm): regenerate chart README for version bump 2.0.14
Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com
下载附件