SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
8.3 KB · 153 lines typescript
Raw Blame History
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