Files
picoclaw/web/frontend/src/components/models/model-validation.ts
T
LC 548dc15acd refactor(models): unify provider metadata around backend catalog (#2896)
* feat(models): unify provider metadata around backend catalog

- Move shared provider metadata and alias normalization into backend-owned provider catalog
- Expose display, fetch, auth, and default model metadata through /api/models provider_options
- Replace frontend static provider registry with catalog-driven selection, validation, grouping, and fallback rendering
- Treat provider default api_base as placeholder and effective fetch/test base while keep submitted api_base separate from derived defaults
- Add model page retry handling, touched locale updates, and provider metadata assertions in backend tests

* fix(models): canonicalize backend provider aliases and common models

* fix(models): restore deepseek common model recommendations
2026-05-20 11:50:34 +08:00

119 lines
3.1 KiB
TypeScript

/**
* Real-time model field validation utilities.
* All checks are pure frontend, no network required.
*
* Messages use i18n keys with interpolation params — callers must
* translate them via t(key, params).
*/
import type { ModelProviderOption } from "@/api/models"
import {
findClosestProvider,
getCanonicalProviderKey,
getKnownProviderKeys,
} from "./provider-registry"
export type ValidationLevel = "error" | "warning" | "success"
export interface FieldValidation {
level: ValidationLevel
messageKey: string
messageParams?: Record<string, string>
fix?: string
}
/**
* Validate a model identifier string with optional provider context.
* Returns validation result with optional one-click fix suggestion.
*/
export function validateModelField(
input: string,
selectedProvider?: string,
backendOptions?: ModelProviderOption[],
): FieldValidation {
const trimmed = input.trim()
if (!trimmed) return { level: "success", messageKey: "" }
const knownProviderKeys = getKnownProviderKeys(backendOptions)
// Hard errors
if (/\s/.test(trimmed)) {
return {
level: "error",
messageKey: "models.validation.whitespace",
fix: trimmed.replace(/\s+/g, "/"),
}
}
if (trimmed.startsWith("/")) {
return {
level: "error",
messageKey: "models.validation.leadingSlash",
fix: trimmed.replace(/^\/+/, ""),
}
}
if (trimmed.includes("//")) {
return {
level: "error",
messageKey: "models.validation.consecutiveSlash",
fix: trimmed.replace(/\/+/g, "/"),
}
}
const slashIdx = trimmed.indexOf("/")
if (slashIdx === -1) {
// No provider prefix — when a provider is already selected,
// the model ID is provider-local and needs no prefix.
if (selectedProvider) {
return {
level: "success",
messageKey: "models.validation.parsed",
messageParams: { provider: selectedProvider, model: trimmed },
}
}
return {
level: "warning",
messageKey: "models.validation.defaultToOpenAI",
fix: `openai/${trimmed}`,
}
}
const provider = trimmed.slice(0, slashIdx)
const model = trimmed.slice(slashIdx + 1)
if (!model) {
return { level: "error", messageKey: "models.validation.emptyModel" }
}
if (!knownProviderKeys.has(provider)) {
// Check aliases
const alias = getCanonicalProviderKey(provider, backendOptions)
if (alias && alias !== provider) {
return {
level: "warning",
messageKey: "models.validation.shouldUse",
messageParams: { provider, alias },
fix: `${alias}/${model}`,
}
}
// Typo check
const closest = findClosestProvider(provider, backendOptions)
if (closest) {
return {
level: "warning",
messageKey: "models.validation.didYouMean",
messageParams: { closest },
fix: `${closest}/${model}`,
}
}
return {
level: "warning",
messageKey: "models.validation.unknownProvider",
messageParams: { provider },
}
}
return {
level: "success",
messageKey: "models.validation.parsed",
messageParams: { provider, model },
}
}