发布

  • [OPIK-5021] [BE][FE] feat: fetch LLM model list from backend registry with CDN-driven labels (#6410)

    frostbyte_neo 发布于 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 a label field 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 label field 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 reasoning flag 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 commit 727fe1e8c2 for
      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; qualifiedName participates in routing lookups
      only. The prior phrasing "(or qualifiedName for Vertex AI)" was wrong —
      LlmModelRegistryService.merge() always dedupes by m.id().
    • Guard getProviderFromModel against snapshot keys that don't match a
      known PROVIDER_TYPE (e.g. a future CDN-added provider key that the FE
      enum doesn't know yet). Downstream reads like PROVIDERS[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.

    1. Memoization. getProviderModels() returned a fresh object each call,
      so every consumer that put providerModels in a useMemo dep
      invalidated the memo every render. Now the hook builds the map via
      useMemo once and exposes it as providerModels; getProviderModels
      stays as a compat wrapper returning the same memoized reference.
      Hot consumers (useModelOptions, OptimizationModelSelect,
      usePromptModelDisplay) now take / read providerModels directly,
      so filter/search pipelines no longer rebuild on every keystroke.

    2. Hydration-window correctness. The module-level snapshot/flag cells
      started as {} / empty Map. Cold-load reads of
      getProviderFromModel / isReasoningModel returned 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.

    3. Store ownership. Module-level mutable state is now in a dedicated
      lib/modelRegistryStore.ts with an explicit resetForTesting()
      export. Makes the bridge between React Query data and pure utility
      functions visible, testable, and cycle-free.

    4. Circular import. hooks/useLLMProviderModelsData.ts imports
      parseComposedProviderType from lib/provider.ts;
      lib/provider.ts previously imported getLatestProviderModelsSnapshot
      back from the hook. Broken by routing the getter through the new
      store module — neither side depends on the other at module eval.

    5. Expose query state. The hook now returns isPending, isError,
      error from the underlying useLlmModels query so picker
      components can render skeletons instead of empty lists during the
      hydration window. Consumers can opt in incrementally.

    6. 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:

    1. Dropdown was leaking non-curated entries. The YAML superset fix from
      an earlier commit meant transformFetched was 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 a label and non-curated don't — use that as
      the filter signal. The store still receives the FULL registry (so
      getProviderFromModel resolves every routable id), the dropdown
      only gets labelled entries.

    2. setLatestProviderModelsSnapshot was clobbering the seeded catalog.
      The effect ran on first mount with fetched=undefined and 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
      fetched is still undefined, merge onto the existing seeded
      snapshot instead of replacing it.

    3. calculateModelProvider didn't guard against unknown snapshot
      keys. Added the same KNOWN_PROVIDER_TYPES check getProviderFromModel
      uses, via parseComposedProviderType so composed keys like
      custom-llm:acme still 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
    • calculateModelProvider now consults a full, unfiltered
      fullProviderModels memo for resolution. The dropdown-facing
      providerModels is 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-20260205 resolves to "" and temperature gating /
      provider-key lookup silently fail.

    • Match persisted bare ids against qualified-name values so a saved
      gemini-2.5-pro selection still resolves when the Vertex AI registry
      entry carries vertex_ai/gemini-2.5-pro. Covers the persistence
      compatibility gap between m.id and m.qualifiedName.

    • Move lodash/first above 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, remoteUrl empty
    • 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 exact m.value match across every provider, so a stored
    vertex_ai/gemini-2.5-pro never 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 into src/constants/providerModels.ts.
    lib/modelRegistryStore.ts imports from there instead of the hook,
    resolving the four no-circular violations 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 two import {...} 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: true on 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 via pre-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

    下载附件