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%
4.3 KB · 87 lines typescript
Raw Blame History
1import type { ModelParameters, PolyModel, UnifiedGenerationSettings } from "./types";23export interface DroppedParameter {4  name: keyof UnifiedGenerationSettings;5  reason: string;6}78/**9 * Capability-driven parameter filter: given the unified settings and the model's10 * parameter sheet, keep only what the model accepts and clamp ranges. Adapters call this11 * BEFORE translating to provider fields so unsupported options are never sent.12 */13export function filterSettings(settings: UnifiedGenerationSettings | undefined, model: PolyModel | undefined): { settings: UnifiedGenerationSettings; dropped: DroppedParameter[] } {14  const s: UnifiedGenerationSettings = { ...(settings ?? {}) };15  const dropped: DroppedParameter[] = [];16  const p: ModelParameters = model?.parameters ?? {};17  const caps = model?.capabilities;1819  const drop = (name: keyof UnifiedGenerationSettings, reason: string) => {20    if (s[name] !== undefined) {21      delete s[name];22      dropped.push({ name, reason });23    }24  };2526  if (!model) return { settings: s, dropped };2728  if (!p.temperature) drop("temperature", "not supported by this model");29  else if (s.temperature !== undefined) {30    const r = p.temperatureRange ?? { min: 0, max: 2 };31    s.temperature = clamp(s.temperature, r.min, r.max);32  }33  if (!p.topP) drop("topP", "not supported by this model");34  else if (s.topP !== undefined) s.topP = clamp(s.topP, 0, 1);35  if (!p.topK) drop("topK", "not supported by this model");36  if (!p.maxTokens) drop("maxTokens", "not supported by this model");37  else if (s.maxTokens !== undefined && model.limits?.maxOutputTokens) s.maxTokens = Math.min(Math.max(1, Math.floor(s.maxTokens)), model.limits.maxOutputTokens);38  if (!p.stop) drop("stop", "not supported by this model");39  if (!p.seed) drop("seed", "not supported by this model");40  if (!p.frequencyPenalty) drop("frequencyPenalty", "not supported by this model");41  if (!p.presencePenalty) drop("presencePenalty", "not supported by this model");42  if (!p.verbosity) drop("verbosity", "not supported by this model");43  if (!p.reasoningEffort) drop("reasoningEffort", "model has no adjustable reasoning effort");44  else if (s.reasoningEffort && p.reasoningEffortLevels && !p.reasoningEffortLevels.includes(s.reasoningEffort)) {45    // snap to the closest accepted level rather than sending an invalid value46    s.reasoningEffort = snapEffort(s.reasoningEffort, p.reasoningEffortLevels);47  }48  if (!p.thinkingBudget) drop("thinkingBudget", "model has no thinking budget");49  else if (s.thinkingBudget !== undefined && p.thinkingBudgetRange) s.thinkingBudget = clamp(Math.floor(s.thinkingBudget), p.thinkingBudgetRange.min, p.thinkingBudgetRange.max);50  if (!caps?.reasoning) drop("includeReasoning", "model does not expose reasoning");51  if (!caps?.structuredOutput && s.responseFormat && s.responseFormat.type !== "text") drop("responseFormat", "structured output not supported");52  if (!caps?.tools) drop("toolChoice", "tools not supported");53  if (!caps?.webSearch) drop("webSearch", "web search not supported");54  if (!caps?.tools && s.codeExecution) drop("codeExecution", "code execution not supported");5556  return { settings: s, dropped };57}5859const EFFORT_ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;6061export function snapEffort(effort: string, levels: string[]): UnifiedGenerationSettings["reasoningEffort"] {62  const idx = EFFORT_ORDER.indexOf(effort as (typeof EFFORT_ORDER)[number]);63  if (idx < 0) return levels[Math.floor(levels.length / 2)] as UnifiedGenerationSettings["reasoningEffort"];64  const ordered = EFFORT_ORDER.filter((l) => levels.includes(l));65  if (ordered.length === 0) return undefined;66  // Closest accepted level; on a tie prefer the HIGHER one so a reasoning request never silently becomes "none".67  let best = ordered[0];68  let bestDist = Infinity;69  for (const l of ordered) {70    const d = Math.abs(EFFORT_ORDER.indexOf(l) - idx);71    if (d < bestDist || (d === bestDist && EFFORT_ORDER.indexOf(l) > EFFORT_ORDER.indexOf(best))) {72      best = l;73      bestDist = d;74    }75  }76  return best as UnifiedGenerationSettings["reasoningEffort"];77}7879export function clamp(n: number, min: number, max: number): number {80  return Math.min(max, Math.max(min, n));81}8283/** Rough heuristic (~4 chars per token) used when a provider has no count endpoint. */84export function heuristicTokens(text: string): number {85  return Math.ceil(text.length / 4);86}87