feat(frontend): Wire LLM picker to live registry

- Extract useLlmModelField hook that fetches live model list from
  /api/v2/llm/models and merges is_enabled/is_recommended flags
- Registry is sole source of truth: models absent or disabled are hidden
- recommended model driven by is_recommended flag, falls back to schema.default
- Simplify LlmModelField.tsx to thin render component
This commit is contained in:
Bentlybro
2026-04-07 22:53:10 +01:00
parent 2951d2ce67
commit e1230db442
3 changed files with 70 additions and 35 deletions

View File

@@ -7,45 +7,16 @@ import {
RJSFSchema,
titleId,
} from "@rjsf/utils";
import { useMemo } from "react";
import { LlmModelPicker } from "./components/LlmModelPicker";
import { LlmModelMetadataMap } from "./types";
import { updateUiOption } from "../../helpers";
type LlmModelSchema = RJSFSchema & {
llm_model_metadata?: LlmModelMetadataMap;
};
import { useLlmModelField } from "./useLlmModelField";
export function LlmModelField(props: FieldProps) {
const { schema, formData, onChange, disabled, readonly, fieldPathId } = props;
const metadata = useMemo(() => {
return (schema as LlmModelSchema)?.llm_model_metadata ?? {};
}, [schema]);
const models = useMemo(() => {
return Object.values(metadata);
}, [metadata]);
const selectedName =
typeof formData === "string"
? formData
: typeof schema.default === "string"
? schema.default
: "";
const selectedModel = selectedName
? (metadata[selectedName] ??
models.find((model) => model.name === selectedName))
: undefined;
const recommendedName =
typeof schema.default === "string" ? schema.default : models[0]?.name;
const recommendedModel =
recommendedName && metadata[recommendedName]
? metadata[recommendedName]
: undefined;
const { models, selectedModel, recommendedModel } = useLlmModelField(
schema as RJSFSchema,
formData,
);
if (models.length === 0) {
return null;
@@ -56,7 +27,6 @@ export function LlmModelField(props: FieldProps) {
"DescriptionFieldTemplate",
props.registry,
);
const updatedUiSchema = updateUiOption(props.uiSchema, {
showHandles: false,
});

View File

@@ -6,6 +6,8 @@ export type LlmModelMetadata = {
provider_name: string;
name: string;
price_tier?: number;
is_recommended?: boolean;
is_enabled?: boolean;
};
export type LlmModelMetadataMap = Record<string, LlmModelMetadata>;

View File

@@ -0,0 +1,63 @@
import { useMemo } from "react";
import { RJSFSchema } from "@rjsf/utils";
import { useGetV2ListModels } from "@/app/api/__generated__/endpoints/llm/llm";
import type { LlmModelsResponse } from "@/app/api/__generated__/models/llmModelsResponse";
import { LlmModelMetadata, LlmModelMetadataMap } from "./types";
type LlmModelSchema = RJSFSchema & {
llm_model_metadata?: LlmModelMetadataMap;
};
export function useLlmModelField(schema: RJSFSchema, formData: unknown) {
const { data: registryData } = useGetV2ListModels(
{ enabled_only: false },
{ query: { staleTime: 60_000 } },
);
const schemaMetadata = useMemo(
() => (schema as LlmModelSchema)?.llm_model_metadata ?? {},
[schema],
);
// Merge live is_enabled / is_recommended flags from the registry into the
// static schema metadata so the picker reflects admin changes without a
// server restart. Models absent from the registry are hidden.
const models = useMemo<LlmModelMetadata[]>(() => {
const responseData = registryData?.data;
const registryModels: LlmModelsResponse["models"] =
responseData && "models" in responseData ? responseData.models : [];
const bySlug = new Map(registryModels.map((m) => [m.slug, m]));
return Object.values(schemaMetadata)
.map((m) => {
const live = bySlug.get(m.name);
return {
...m,
// Registry is the sole source of truth; absent = not shown.
is_enabled: live?.is_enabled ?? false,
is_recommended: live?.is_recommended ?? false,
};
})
.filter((m) => m.is_enabled !== false);
}, [schemaMetadata, registryData]);
const selectedName =
typeof formData === "string"
? formData
: typeof schema.default === "string"
? schema.default
: "";
const selectedModel = selectedName
? models.find((m) => m.name === selectedName)
: undefined;
// Registry flag takes priority; schema.default is the static fallback.
const recommendedModel =
models.find((m) => m.is_recommended) ??
models.find(
(m) => typeof schema.default === "string" && m.name === schema.default,
);
return { models, selectedModel, recommendedModel };
}