TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type { PolyModel } from "@/lib/ai/core/types";23/**4 * Capability-aware parameter sheet shared by the settings panel, the model profile and presets.5 * One definition per unified setting: which group it belongs to, whether a model supports it and6 * a short detail string (ranges, levels). Components never branch on model ids — only on this.7 */8export type SettingsGroup = "generation" | "reasoning" | "output" | "tools" | "advanced";910export const SETTINGS_GROUPS: { key: SettingsGroup; label: string; description: string }[] = [11 { key: "generation", label: "Generation", description: "Sampling and length" },12 { key: "reasoning", label: "Reasoning", description: "Thinking effort and budget" },13 { key: "output", label: "Output", description: "Format, verbosity, stop sequences" },14 { key: "tools", label: "Tools", description: "Web search, code execution, built-in tools" },15 { key: "advanced", label: "Advanced", description: "Seed and penalties" },16];1718const CODE_EXEC_PROVIDERS = new Set(["openai", "anthropic", "gemini"]);1920export interface ParamDef {21 key: string;22 label: string;23 group: SettingsGroup;24 supports: (m: PolyModel) => boolean;25 detail?: (m: PolyModel) => string | null;26}2728function fmt(n: number): string {29 return n >= 1000 ? `${(n / 1000).toFixed(n % 1000 ? 1 : 0)}K` : String(n);30}3132export const PARAM_DEFS: ParamDef[] = [33 { 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") },34 { key: "topP", label: "Top-p", group: "generation", supports: (m) => Boolean(m.parameters.topP), detail: () => "0–1" },35 { key: "topK", label: "Top-k", group: "generation", supports: (m) => Boolean(m.parameters.topK) },36 { 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) },37 { key: "reasoningEffort", label: "Reasoning effort", group: "reasoning", supports: (m) => Boolean(m.parameters.reasoningEffort), detail: (m) => m.parameters.reasoningEffortLevels?.join(" / ") ?? null },38 { 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) },39 { key: "includeReasoning", label: "Show reasoning", group: "reasoning", supports: (m) => m.capabilities.reasoning },40 { key: "verbosity", label: "Verbosity", group: "output", supports: (m) => Boolean(m.parameters.verbosity), detail: () => "low / medium / high" },41 { 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") },42 { key: "stop", label: "Stop sequences", group: "output", supports: (m) => Boolean(m.parameters.stop), detail: () => "up to 4" },43 { key: "webSearch", label: "Web search", group: "tools", supports: (m) => m.capabilities.webSearch },44 { 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)) },45 { key: "tools", label: "Built-in tools", group: "tools", supports: (m) => m.capabilities.tools },46 { key: "toolChoice", label: "Tool choice", group: "tools", supports: (m) => m.capabilities.tools },47 { key: "seed", label: "Seed", group: "advanced", supports: (m) => Boolean(m.parameters.seed) },48 { key: "frequencyPenalty", label: "Frequency penalty", group: "advanced", supports: (m) => Boolean(m.parameters.frequencyPenalty), detail: () => "−2–2" },49 { key: "presencePenalty", label: "Presence penalty", group: "advanced", supports: (m) => Boolean(m.parameters.presencePenalty), detail: () => "−2–2" },50];5152const DEF_BY_KEY = new Map(PARAM_DEFS.map((d) => [d.key, d]));5354export function paramDef(key: string): ParamDef | undefined {55 return DEF_BY_KEY.get(key);56}5758export function supportsParam(m: PolyModel, key: string): boolean {59 const d = DEF_BY_KEY.get(key);60 return d ? d.supports(m) : false;61}6263export function supportedParams(m: PolyModel): ParamDef[] {64 return PARAM_DEFS.filter((d) => d.supports(m));65}6667export function groupsFor(m: PolyModel): { group: (typeof SETTINGS_GROUPS)[number]; defs: ParamDef[] }[] {68 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);69}7071/** Is a setting value "set" (worth validating / counting)? */72export function isSetValue(key: string, v: unknown): boolean {73 if (v === undefined || v === null) return false;74 if (Array.isArray(v)) return v.length > 0;75 if (key === "responseFormat") return typeof v === "object" && (v as { type?: string }).type !== undefined && (v as { type?: string }).type !== "text";76 if (key === "includeReasoning") return v === false; // default is on; only an explicit "off" counts77 if (key === "toolChoice") return v !== "auto";78 if (typeof v === "boolean") return v;79 return true;80}8182export interface UnsupportedSetting {83 key: string;84 label: string;85 reason: string;86}8788/**89 * Settings the model cannot accept (they would be dropped by `filterSettings()` server-side).90 * Includes an effort level the model does not offer.91 */92export function unsupportedSettings(settings: Record<string, unknown>, m: PolyModel): UnsupportedSetting[] {93 const out: UnsupportedSetting[] = [];94 for (const [key, v] of Object.entries(settings)) {95 if (!isSetValue(key, v)) continue;96 const def = DEF_BY_KEY.get(key);97 if (!def) continue; // unknown keys are ignored (providerOptions etc.)98 if (!def.supports(m)) {99 out.push({ key, label: def.label, reason: `${def.label} is not supported by ${m.displayName}` });100 continue;101 }102 if (key === "reasoningEffort" && typeof v === "string") {103 const levels = m.parameters.reasoningEffortLevels;104 if (levels?.length && !levels.includes(v)) out.push({ key, label: def.label, reason: `Effort “${v}” is not offered (${levels.join(" / ")})` });105 }106 if (key === "responseFormat" && typeof v === "object" && (v as { type?: string }).type === "json_schema" && m.metadata?.jsonSchema === false) {107 out.push({ key, label: def.label, reason: `${m.displayName} accepts JSON objects but not JSON schemas` });108 }109 if (key === "thinkingBudget" && typeof v === "number" && m.parameters.thinkingBudgetRange) {110 const r = m.parameters.thinkingBudgetRange;111 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` });112 }113 }114 return out;115}116117export interface PresetLike {118 parameters?: Record<string, unknown> | null;119 tools?: Record<string, unknown> | null;120 systemPrompt?: string | null;121}122123/** Flatten a stored preset into a settings object (tools live in `tools.builtin`). */124export function presetToSettings(p: PresetLike): Record<string, unknown> {125 const s: Record<string, unknown> = { ...(p.parameters ?? {}) };126 const builtin = (p.tools as { builtin?: string[] } | null | undefined)?.builtin;127 if (builtin?.length) s.tools = builtin;128 return s;129}130131export interface Compatibility {132 compatible: boolean;133 unsupported: UnsupportedSetting[];134 /** Number of settings the preset defines. */135 total: number;136}137138/** A preset is compatible when every parameter it defines is supported by the model. */139export function presetCompatibility(p: PresetLike, m: PolyModel): Compatibility {140 const s = presetToSettings(p);141 const total = Object.entries(s).filter(([k, v]) => isSetValue(k, v)).length;142 const unsupported = unsupportedSettings(s, m);143 return { compatible: unsupported.length === 0, unsupported, total };144}145146/** Drop settings the model does not support (client-side mirror of the server's `filterSettings`). */147export function pruneSettingsForModel<T extends Record<string, unknown>>(settings: T, m: PolyModel | null | undefined): T {148 if (!m) return settings;149 const out = { ...settings } as Record<string, unknown>;150 for (const u of unsupportedSettings(out, m)) delete out[u.key];151 return out as T;152}153