import type { PolyModel } from "@/lib/ai/core/types"; /** * Capability-aware parameter sheet shared by the settings panel, the model profile and presets. * One definition per unified setting: which group it belongs to, whether a model supports it and * a short detail string (ranges, levels). Components never branch on model ids — only on this. */ export type SettingsGroup = "generation" | "reasoning" | "output" | "tools" | "advanced"; export const SETTINGS_GROUPS: { key: SettingsGroup; label: string; description: string }[] = [ { key: "generation", label: "Generation", description: "Sampling and length" }, { key: "reasoning", label: "Reasoning", description: "Thinking effort and budget" }, { key: "output", label: "Output", description: "Format, verbosity, stop sequences" }, { key: "tools", label: "Tools", description: "Web search, code execution, built-in tools" }, { key: "advanced", label: "Advanced", description: "Seed and penalties" }, ]; const CODE_EXEC_PROVIDERS = new Set(["openai", "anthropic", "gemini"]); export interface ParamDef { key: string; label: string; group: SettingsGroup; supports: (m: PolyModel) => boolean; detail?: (m: PolyModel) => string | null; } function fmt(n: number): string { return n >= 1000 ? `${(n / 1000).toFixed(n % 1000 ? 1 : 0)}K` : String(n); } export const PARAM_DEFS: ParamDef[] = [ { key: "temperature", label: "Temperature", group: "generation", supports: (m) => Boolean(m.parameters.temperature), detail: (m) => (m.parameters.temperatureRange ? `${m.parameters.temperatureRange.min}–${m.parameters.temperatureRange.max}` : "0–2") }, { key: "topP", label: "Top-p", group: "generation", supports: (m) => Boolean(m.parameters.topP), detail: () => "0–1" }, { key: "topK", label: "Top-k", group: "generation", supports: (m) => Boolean(m.parameters.topK) }, { key: "maxTokens", label: "Max output tokens", group: "generation", supports: (m) => Boolean(m.parameters.maxTokens), detail: (m) => (m.limits?.maxOutputTokens ? `up to ${fmt(m.limits.maxOutputTokens)}` : null) }, { key: "reasoningEffort", label: "Reasoning effort", group: "reasoning", supports: (m) => Boolean(m.parameters.reasoningEffort), detail: (m) => m.parameters.reasoningEffortLevels?.join(" / ") ?? null }, { key: "thinkingBudget", label: "Thinking budget", group: "reasoning", supports: (m) => Boolean(m.parameters.thinkingBudget), detail: (m) => (m.parameters.thinkingBudgetRange ? `${fmt(m.parameters.thinkingBudgetRange.min)}–${fmt(m.parameters.thinkingBudgetRange.max)} tokens` : null) }, { key: "includeReasoning", label: "Show reasoning", group: "reasoning", supports: (m) => m.capabilities.reasoning }, { key: "verbosity", label: "Verbosity", group: "output", supports: (m) => Boolean(m.parameters.verbosity), detail: () => "low / medium / high" }, { key: "responseFormat", label: "Structured output", group: "output", supports: (m) => m.capabilities.structuredOutput, detail: (m) => (m.metadata?.jsonSchema === false ? "JSON object only" : "JSON object / JSON schema") }, { key: "stop", label: "Stop sequences", group: "output", supports: (m) => Boolean(m.parameters.stop), detail: () => "up to 4" }, { key: "webSearch", label: "Web search", group: "tools", supports: (m) => m.capabilities.webSearch }, { key: "codeExecution", label: "Code execution", group: "tools", supports: (m) => m.metadata?.codeExecution === true || (m.metadata?.codeExecution !== false && m.capabilities.tools && CODE_EXEC_PROVIDERS.has(m.provider)) }, { key: "tools", label: "Built-in tools", group: "tools", supports: (m) => m.capabilities.tools }, { key: "toolChoice", label: "Tool choice", group: "tools", supports: (m) => m.capabilities.tools }, { key: "seed", label: "Seed", group: "advanced", supports: (m) => Boolean(m.parameters.seed) }, { key: "frequencyPenalty", label: "Frequency penalty", group: "advanced", supports: (m) => Boolean(m.parameters.frequencyPenalty), detail: () => "−2–2" }, { key: "presencePenalty", label: "Presence penalty", group: "advanced", supports: (m) => Boolean(m.parameters.presencePenalty), detail: () => "−2–2" }, ]; const DEF_BY_KEY = new Map(PARAM_DEFS.map((d) => [d.key, d])); export function paramDef(key: string): ParamDef | undefined { return DEF_BY_KEY.get(key); } export function supportsParam(m: PolyModel, key: string): boolean { const d = DEF_BY_KEY.get(key); return d ? d.supports(m) : false; } export function supportedParams(m: PolyModel): ParamDef[] { return PARAM_DEFS.filter((d) => d.supports(m)); } export function groupsFor(m: PolyModel): { group: (typeof SETTINGS_GROUPS)[number]; defs: ParamDef[] }[] { return SETTINGS_GROUPS.map((g) => ({ group: g, defs: PARAM_DEFS.filter((d) => d.group === g.key && d.supports(m)) })).filter((g) => g.defs.length > 0); } /** Is a setting value "set" (worth validating / counting)? */ export function isSetValue(key: string, v: unknown): boolean { if (v === undefined || v === null) return false; if (Array.isArray(v)) return v.length > 0; if (key === "responseFormat") return typeof v === "object" && (v as { type?: string }).type !== undefined && (v as { type?: string }).type !== "text"; if (key === "includeReasoning") return v === false; // default is on; only an explicit "off" counts if (key === "toolChoice") return v !== "auto"; if (typeof v === "boolean") return v; return true; } export interface UnsupportedSetting { key: string; label: string; reason: string; } /** * Settings the model cannot accept (they would be dropped by `filterSettings()` server-side). * Includes an effort level the model does not offer. */ export function unsupportedSettings(settings: Record, m: PolyModel): UnsupportedSetting[] { const out: UnsupportedSetting[] = []; for (const [key, v] of Object.entries(settings)) { if (!isSetValue(key, v)) continue; const def = DEF_BY_KEY.get(key); if (!def) continue; // unknown keys are ignored (providerOptions etc.) if (!def.supports(m)) { out.push({ key, label: def.label, reason: `${def.label} is not supported by ${m.displayName}` }); continue; } if (key === "reasoningEffort" && typeof v === "string") { const levels = m.parameters.reasoningEffortLevels; if (levels?.length && !levels.includes(v)) out.push({ key, label: def.label, reason: `Effort “${v}” is not offered (${levels.join(" / ")})` }); } if (key === "responseFormat" && typeof v === "object" && (v as { type?: string }).type === "json_schema" && m.metadata?.jsonSchema === false) { out.push({ key, label: def.label, reason: `${m.displayName} accepts JSON objects but not JSON schemas` }); } if (key === "thinkingBudget" && typeof v === "number" && m.parameters.thinkingBudgetRange) { const r = m.parameters.thinkingBudgetRange; if (v < r.min || v > r.max) out.push({ key, label: def.label, reason: `Budget must be between ${fmt(r.min)} and ${fmt(r.max)} tokens` }); } } return out; } export interface PresetLike { parameters?: Record | null; tools?: Record | null; systemPrompt?: string | null; } /** Flatten a stored preset into a settings object (tools live in `tools.builtin`). */ export function presetToSettings(p: PresetLike): Record { const s: Record = { ...(p.parameters ?? {}) }; const builtin = (p.tools as { builtin?: string[] } | null | undefined)?.builtin; if (builtin?.length) s.tools = builtin; return s; } export interface Compatibility { compatible: boolean; unsupported: UnsupportedSetting[]; /** Number of settings the preset defines. */ total: number; } /** A preset is compatible when every parameter it defines is supported by the model. */ export function presetCompatibility(p: PresetLike, m: PolyModel): Compatibility { const s = presetToSettings(p); const total = Object.entries(s).filter(([k, v]) => isSetValue(k, v)).length; const unsupported = unsupportedSettings(s, m); return { compatible: unsupported.length === 0, unsupported, total }; } /** Drop settings the model does not support (client-side mirror of the server's `filterSettings`). */ export function pruneSettingsForModel>(settings: T, m: PolyModel | null | undefined): T { if (!m) return settings; const out = { ...settings } as Record; for (const u of unsupportedSettings(out, m)) delete out[u.key]; return out as T; }