import type { ModelParameters, PolyModel, UnifiedGenerationSettings } from "./types"; export interface DroppedParameter { name: keyof UnifiedGenerationSettings; reason: string; } /** * Capability-driven parameter filter: given the unified settings and the model's * parameter sheet, keep only what the model accepts and clamp ranges. Adapters call this * BEFORE translating to provider fields so unsupported options are never sent. */ export function filterSettings(settings: UnifiedGenerationSettings | undefined, model: PolyModel | undefined): { settings: UnifiedGenerationSettings; dropped: DroppedParameter[] } { const s: UnifiedGenerationSettings = { ...(settings ?? {}) }; const dropped: DroppedParameter[] = []; const p: ModelParameters = model?.parameters ?? {}; const caps = model?.capabilities; const drop = (name: keyof UnifiedGenerationSettings, reason: string) => { if (s[name] !== undefined) { delete s[name]; dropped.push({ name, reason }); } }; if (!model) return { settings: s, dropped }; if (!p.temperature) drop("temperature", "not supported by this model"); else if (s.temperature !== undefined) { const r = p.temperatureRange ?? { min: 0, max: 2 }; s.temperature = clamp(s.temperature, r.min, r.max); } if (!p.topP) drop("topP", "not supported by this model"); else if (s.topP !== undefined) s.topP = clamp(s.topP, 0, 1); if (!p.topK) drop("topK", "not supported by this model"); if (!p.maxTokens) drop("maxTokens", "not supported by this model"); else if (s.maxTokens !== undefined && model.limits?.maxOutputTokens) s.maxTokens = Math.min(Math.max(1, Math.floor(s.maxTokens)), model.limits.maxOutputTokens); if (!p.stop) drop("stop", "not supported by this model"); if (!p.seed) drop("seed", "not supported by this model"); if (!p.frequencyPenalty) drop("frequencyPenalty", "not supported by this model"); if (!p.presencePenalty) drop("presencePenalty", "not supported by this model"); if (!p.verbosity) drop("verbosity", "not supported by this model"); if (!p.reasoningEffort) drop("reasoningEffort", "model has no adjustable reasoning effort"); else if (s.reasoningEffort && p.reasoningEffortLevels && !p.reasoningEffortLevels.includes(s.reasoningEffort)) { // snap to the closest accepted level rather than sending an invalid value s.reasoningEffort = snapEffort(s.reasoningEffort, p.reasoningEffortLevels); } if (!p.thinkingBudget) drop("thinkingBudget", "model has no thinking budget"); else if (s.thinkingBudget !== undefined && p.thinkingBudgetRange) s.thinkingBudget = clamp(Math.floor(s.thinkingBudget), p.thinkingBudgetRange.min, p.thinkingBudgetRange.max); if (!caps?.reasoning) drop("includeReasoning", "model does not expose reasoning"); if (!caps?.structuredOutput && s.responseFormat && s.responseFormat.type !== "text") drop("responseFormat", "structured output not supported"); if (!caps?.tools) drop("toolChoice", "tools not supported"); if (!caps?.webSearch) drop("webSearch", "web search not supported"); if (!caps?.tools && s.codeExecution) drop("codeExecution", "code execution not supported"); return { settings: s, dropped }; } const EFFORT_ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const; export function snapEffort(effort: string, levels: string[]): UnifiedGenerationSettings["reasoningEffort"] { const idx = EFFORT_ORDER.indexOf(effort as (typeof EFFORT_ORDER)[number]); if (idx < 0) return levels[Math.floor(levels.length / 2)] as UnifiedGenerationSettings["reasoningEffort"]; const ordered = EFFORT_ORDER.filter((l) => levels.includes(l)); if (ordered.length === 0) return undefined; // Closest accepted level; on a tie prefer the HIGHER one so a reasoning request never silently becomes "none". let best = ordered[0]; let bestDist = Infinity; for (const l of ordered) { const d = Math.abs(EFFORT_ORDER.indexOf(l) - idx); if (d < bestDist || (d === bestDist && EFFORT_ORDER.indexOf(l) > EFFORT_ORDER.indexOf(best))) { best = l; bestDist = d; } } return best as UnifiedGenerationSettings["reasoningEffort"]; } export function clamp(n: number, min: number, max: number): number { return Math.min(max, Math.max(min, n)); } /** Rough heuristic (~4 chars per token) used when a provider has no count endpoint. */ export function heuristicTokens(text: string): number { return Math.ceil(text.length / 4); }