Download server/src/lib/sampling-params.ts from automindy/fokemal: direct link, hf CLI and curl.
- Browser
- Download file 24.6 kB
-
https://huggingface.co/spaces/automindy/fokemal/resolve/main/server/src/lib/sampling-params.ts
- Command line
-
hf download hf://spaces/automindy/fokemal/server/src/lib/sampling-params.ts
-
curl -L -o sampling-params.ts https://huggingface.co/spaces/automindy/fokemal/resolve/main/server/src/lib/sampling-params.ts
24.6 kB
| // Extended sampling / output-shape parameters forwarded to providers. | |
| // | |
| // Until now only temperature / max_tokens / top_p / stop reached upstream β | |
| // everything else a client sent (seed, penalties, logit_bias, logprobs, | |
| // response_formatβ¦) was validated away by the request schema and silently | |
| // dropped, so "OpenAI-compatible" was quietly narrower than it claimed | |
| // (structured outputs simply did not work). This module is the single source | |
| // of truth for the extended set: | |
| // | |
| // - the zod fields each surface spreads into its request schema, | |
| // - the pick helper that turns a parsed body into CompletionOptions fields, | |
| // - the per-platform support policy (which params a provider is known to | |
| // reject or ignore) used both by the adapters (drop before send) and by | |
| // /v1/models `supported_parameters` (advertise per model). | |
| // | |
| // Forward-by-default: most OpenAI-compatible providers ignore unknown body | |
| // fields, so the default is to send everything and let the per-platform | |
| // droplist name the documented exceptions (Mistral 422s on unknown keys, | |
| // Groq 400s on the logprobs family, Azure-backed GitHub Models rejects | |
| // non-Azure knobs). A provider that still 400s fails over like any other | |
| // provider-invalid request and shows up in the attempt trail β and its | |
| // droplist entry is one line to add. | |
| import { z } from 'zod'; | |
| import type { Platform } from '@freellmapi/shared/types.js'; | |
| import { getSetting } from '../db/index.js'; | |
| // OpenAI's request-side reasoning knob. Wire values as of the current OpenAI | |
| // API: 'minimal'|'low'|'medium'|'high', plus 'none' (gpt-5.1). Forwarded | |
| // verbatim to openai-compat platforms per the policy below; the Google | |
| // adapter maps it natively onto generationConfig.thinkingConfig. | |
| export const REASONING_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high'] as const; | |
| export type ReasoningEffort = typeof REASONING_EFFORTS[number]; | |
| // Effort spellings clients actually send that aren't on OpenAI's scale, mapped | |
| // to the nearest value we do support (#619: a strict enum turned a client's | |
| // `reasoning_effort: 'max'` into a 400 from our own edge β a knob nobody asked | |
| // to be fatal). Values that mean "let the model decide" ('auto', 'adaptive', | |
| // 'default') are deliberately absent: they resolve to undefined below, i.e. | |
| // nothing is forwarded and the provider default stands β same rule | |
| // effortFromGeminiThinking applies to a -1 thinking budget. | |
| const EFFORT_ALIASES: Readonly<Record<string, ReasoningEffort>> = { | |
| max: 'high', maximum: 'high', highest: 'high', ultra: 'high', xhigh: 'high', 'x-high': 'high', | |
| mid: 'medium', moderate: 'medium', balanced: 'medium', normal: 'medium', standard: 'medium', | |
| min: 'minimal', minimum: 'minimal', lowest: 'minimal', xlow: 'minimal', 'x-low': 'minimal', | |
| off: 'none', disabled: 'none', disable: 'none', | |
| }; | |
| /** | |
| * Coerce a client-supplied effort value onto our scale: supported values pass | |
| * through, known aliases clamp to the nearest supported one, and anything | |
| * else (unknown word, wrong type, "auto") yields undefined β the knob is | |
| * dropped and the provider's own default applies. Never throws, so a bad | |
| * effort can't fail a request. Exported for tests. | |
| */ | |
| export function normalizeReasoningEffort(value: unknown): ReasoningEffort | undefined { | |
| if (typeof value !== 'string') return undefined; | |
| const key = value.trim().toLowerCase(); | |
| if ((REASONING_EFFORTS as readonly string[]).includes(key)) return key as ReasoningEffort; | |
| return EFFORT_ALIASES[key]; | |
| } | |
| /** | |
| * Clamp an effort to the nearest value a platform actually accepts, walking | |
| * the ordered scale outward from the requested one (ties go to the stronger | |
| * value, so 'medium' on a low/high platform becomes 'high'). Returns | |
| * undefined only when the platform accepts nothing. | |
| */ | |
| function clampEffortTo(effort: ReasoningEffort, supported: readonly ReasoningEffort[]): ReasoningEffort | undefined { | |
| if (supported.includes(effort)) return effort; | |
| const want = REASONING_EFFORTS.indexOf(effort); | |
| let best: ReasoningEffort | undefined; | |
| let bestDistance = Infinity; | |
| for (const candidate of supported) { | |
| const distance = Math.abs(REASONING_EFFORTS.indexOf(candidate) - want); | |
| if (distance < bestDistance | |
| || (distance === bestDistance && best !== undefined | |
| && REASONING_EFFORTS.indexOf(candidate) > REASONING_EFFORTS.indexOf(best))) { | |
| best = candidate; | |
| bestDistance = distance; | |
| } | |
| } | |
| return best; | |
| } | |
| // Every field is `.nullable()` because real clients serialize their whole | |
| // request struct and send explicit nulls for unset knobs (#200); null is | |
| // treated as absent and never forwarded. | |
| export const samplingParamSchemaFields = { | |
| top_k: z.number().int().min(1).nullable().optional(), | |
| min_p: z.number().min(0).max(1).nullable().optional(), | |
| seed: z.number().int().nullable().optional(), | |
| presence_penalty: z.number().min(-2).max(2).nullable().optional(), | |
| frequency_penalty: z.number().min(-2).max(2).nullable().optional(), | |
| repetition_penalty: z.number().positive().nullable().optional(), | |
| logit_bias: z.record(z.string(), z.number()).nullable().optional(), | |
| logprobs: z.boolean().nullable().optional(), | |
| top_logprobs: z.number().int().min(0).max(20).nullable().optional(), | |
| response_format: z.object({ | |
| type: z.enum(['text', 'json_object', 'json_schema']), | |
| json_schema: z.object({ | |
| name: z.string().optional(), | |
| strict: z.boolean().nullable().optional(), | |
| schema: z.record(z.string(), z.unknown()).optional(), | |
| }).passthrough().optional(), | |
| }).passthrough().nullable().optional(), | |
| // Accepted as free-form and normalized by pickSamplingParams rather than | |
| // validated against the enum: clients invent effort values ('max', 'xhigh') | |
| // and rejecting them made an advisory knob fatal (#619). | |
| reasoning_effort: z.unknown().optional(), | |
| // Object-form alias some clients send (OpenRouter-style chat clients, and | |
| // the Responses API's native shape): `reasoning: { effort }`. Resolved into | |
| // reasoning_effort by pickSamplingParams; the wrapper object itself is never | |
| // forwarded. Extra keys (summary, max_tokensβ¦) are tolerated and ignored. | |
| reasoning: z.object({ | |
| effort: z.unknown().optional(), | |
| }).passthrough().nullable().optional(), | |
| // OpenAI's newer alias for max_tokens; surfaces resolve it into max_tokens | |
| // themselves (it is not a forwarded param of its own). | |
| max_completion_tokens: z.number().int().nullable().optional(), | |
| } as const; | |
| export interface ResponseFormat { | |
| type: 'json_object' | 'json_schema'; | |
| json_schema?: { | |
| name?: string; | |
| strict?: boolean | null; | |
| schema?: Record<string, unknown>; | |
| } & Record<string, unknown>; | |
| } | |
| export interface ExtendedSamplingOptions { | |
| top_k?: number; | |
| min_p?: number; | |
| seed?: number; | |
| presence_penalty?: number; | |
| frequency_penalty?: number; | |
| repetition_penalty?: number; | |
| logit_bias?: Record<string, number>; | |
| logprobs?: boolean; | |
| top_logprobs?: number; | |
| response_format?: ResponseFormat; | |
| reasoning_effort?: ReasoningEffort; | |
| } | |
| export const EXTENDED_SAMPLING_KEYS = [ | |
| 'top_k', 'min_p', 'seed', 'presence_penalty', 'frequency_penalty', | |
| 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs', | |
| 'response_format', 'reasoning_effort', | |
| ] as const; | |
| export type ExtendedSamplingKey = typeof EXTENDED_SAMPLING_KEYS[number]; | |
| type ParsedSamplingBody = { | |
| [K in ExtendedSamplingKey]?: unknown; | |
| } & { | |
| // Object-form reasoning alias (see samplingParamSchemaFields); resolved into | |
| // reasoning_effort below, never forwarded as-is. | |
| reasoning?: { effort?: unknown } | null; | |
| }; | |
| /** | |
| * Turn a schema-parsed request body into the extended CompletionOptions | |
| * fields: nulls dropped, `response_format: {type:'text'}` dropped (it is the | |
| * default and some providers 400 on receiving it explicitly), everything else | |
| * forwarded as-is. | |
| */ | |
| export function pickSamplingParams(body: ParsedSamplingBody): ExtendedSamplingOptions { | |
| const out: Record<string, unknown> = {}; | |
| for (const key of EXTENDED_SAMPLING_KEYS) { | |
| const value = body[key]; | |
| if (value === undefined || value === null) continue; | |
| if (key === 'response_format' && (value as { type?: string }).type === 'text') continue; | |
| if (key === 'reasoning_effort') { | |
| const effort = normalizeReasoningEffort(value); | |
| if (effort === undefined) continue; | |
| out[key] = effort; | |
| continue; | |
| } | |
| out[key] = value; | |
| } | |
| // Object-form fallback: `reasoning: { effort }` fills reasoning_effort only | |
| // when the flat field wasn't sent (the explicit flat form wins on conflict). | |
| if (out.reasoning_effort === undefined) { | |
| const effort = normalizeReasoningEffort(body.reasoning?.effort); | |
| if (effort !== undefined) out.reasoning_effort = effort; | |
| } | |
| return out as ExtendedSamplingOptions; | |
| } | |
| // ββ Per-platform support policy ββββββββββββββββββββββββββββββββββββββββββββββ | |
| // `drop` = params this platform is KNOWN to reject (or that would corrupt the | |
| // request); stripped by the adapter before send and omitted from the model's | |
| // advertised `supported_parameters`. `rename` = same param, different wire | |
| // name. Platforms not listed forward everything. | |
| // | |
| // Deliberately conservative: only documented/observed rejections are listed, | |
| // because wrongly dropping a working param is as bad as a 400. Findings from | |
| // live sweeps go here, one line each. | |
| export interface PlatformParamPolicy { | |
| drop?: readonly ExtendedSamplingKey[]; | |
| rename?: Readonly<Partial<Record<ExtendedSamplingKey, string>>>; | |
| // The platform supports json_schema but 400s on json_object (observed live: | |
| // Reka β "Unsupported response_format type: 'json_object'. Supported types | |
| // are 'text' and 'json_schema'."). Upgrade json_object to a permissive | |
| // json_schema on the wire instead of dropping structured output entirely. | |
| jsonObjectToSchema?: boolean; | |
| // The reverse compatibility shim for providers that support JSON mode but | |
| // not schema enforcement. Preserve structured output by requesting a JSON | |
| // object instead of sending an unsupported json_schema payload. | |
| jsonSchemaToObject?: boolean; | |
| // The effort values this platform's API accepts, when it accepts fewer than | |
| // the full scale. A request outside the set is clamped to the nearest | |
| // supported value instead of being forwarded into a 400 (#619). Omitted = | |
| // forward whatever the client asked for. | |
| reasoningEfforts?: readonly ReasoningEffort[]; | |
| // Output-token floor sent when β and ONLY when β the client omitted | |
| // max_tokens entirely (#553). Some providers apply a tiny server-side | |
| // default in that case and cut the answer off mid-sentence, which clients | |
| // experience as a broken stream rather than as a truncation. A | |
| // client-supplied value always wins, larger or smaller. Applied by the | |
| // adapters through resolveMaxTokens(), so a platform added here only takes | |
| // effect once its adapter routes max_tokens through that helper (they all | |
| // do). | |
| defaultMaxTokens?: number; | |
| // Output-token CEILING this platform's API enforces itself, applied by | |
| // resolveMaxTokens() whatever the client asked for. The mirror image of | |
| // defaultMaxTokens: a floor rescues a client that sent nothing, a cap | |
| // rescues one that sent too much. Without it an aggressive max_tokens | |
| // (Open WebUI's 65536 default) is a guaranteed 400 on every hop that lands | |
| // on such a platform, and the fallback chain cannot repair it because the | |
| // same value rides every candidate. Effective max_tokens is min(requested, | |
| // this cap, the operator's unified cap). | |
| maxTokensCap?: number; | |
| } | |
| /** GitHub Models' own output-token ceiling: asking for more 400s ("max_tokens | |
| * is too large"), so the request never reaches the model. Wired into the | |
| * github policy below as maxTokensCap. */ | |
| export const GITHUB_MAX_OUTPUT_TOKENS = 400; | |
| // Keyed by Platform (not string) so a typo'd platform id fails tsc instead of | |
| // silently no-op'ing the policy; the string-typed accessors below cast at the | |
| // boundary since routes carry platform ids as plain strings. | |
| export const PLATFORM_PARAM_POLICIES: Partial<Record<Platform, PlatformParamPolicy>> = { | |
| // Inferbase Free managed inference rejects output budgets above 1,024. | |
| // Match the documented upstream ceiling, not a new application quota. | |
| inferbase: { defaultMaxTokens: 1024, maxTokensCap: 1024 }, | |
| // Gizmo rejects all unlisted fields; reflect its text-only API in discovery | |
| // as well as the adapter. This is the upstream ceiling, not a new quota. | |
| gizmo: { drop: [...EXTENDED_SAMPLING_KEYS], defaultMaxTokens: 1024, maxTokensCap: 32768 }, | |
| // Moondream maps reasoning effort to a boolean and max_tokens to | |
| // max_completion_tokens. 4096 is the documented upstream output ceiling. | |
| moondream: { | |
| drop: ['top_k', 'min_p', 'seed', 'presence_penalty', 'frequency_penalty', 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs', 'response_format'], | |
| maxTokensCap: 4096, | |
| }, | |
| // ACLIDE uses Responses; these Chat Completions parameters have no mapping. | |
| aclide: { | |
| drop: ['top_k', 'min_p', 'seed', 'presence_penalty', 'frequency_penalty', 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs'], | |
| }, | |
| // Sail's stable Responses API accepts temperature/top_p, JSON Schema output, | |
| // tools and reasoning effort. The remaining Chat Completions knobs are not | |
| // supported and are intentionally omitted by the dedicated adapter. | |
| sail: { | |
| drop: ['top_k', 'min_p', 'seed', 'presence_penalty', 'frequency_penalty', 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs'], | |
| jsonObjectToSchema: true, | |
| }, | |
| // Mistral's API is strict (422 on unknown body keys) and names its seed | |
| // `random_seed`. It has no top_k/min_p/logit_bias/logprobs equivalents, and | |
| // no reasoning_effort (Magistral's reasoning has no request-side knob). | |
| mistral: { | |
| drop: ['top_k', 'min_p', 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs', 'reasoning_effort'], | |
| rename: { seed: 'random_seed' }, | |
| }, | |
| // Groq documents logprobs / top_logprobs / logit_bias as unsupported and | |
| // rejects requests that include them. | |
| groq: { drop: ['logprobs', 'top_logprobs', 'logit_bias'] }, | |
| // GitHub Models sits on Azure OpenAI, which 400s "Unrecognized request | |
| // argument" for knobs outside the OpenAI set. Its reasoning_effort enum is | |
| // the older low/medium/high one, so 'none'/'minimal' are clamped rather | |
| // than sent. Its free tier also refuses any max_tokens above | |
| // GITHUB_MAX_OUTPUT_TOKENS, so the cap is clamped here instead of being | |
| // spent as a wasted fallback hop. | |
| github: { | |
| drop: ['top_k', 'min_p', 'repetition_penalty'], | |
| reasoningEfforts: ['low', 'medium', 'high'], | |
| maxTokensCap: GITHUB_MAX_OUTPUT_TOKENS, | |
| }, | |
| // Gemini's generationConfig has no equivalents for these; the adapter | |
| // translates the rest natively (topK, seed, penalties, responseSchema, and | |
| // reasoning_effort β thinkingConfig β see toGeminiExtendedConfig). | |
| google: { drop: ['min_p', 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs'] }, | |
| // Cohere's OpenAI-compat endpoint covers seed/penalties/response_format; | |
| // the rest (incl. reasoning_effort β Cohere's own knob is a non-OpenAI | |
| // `thinking` object) have no mapping there. | |
| cohere: { drop: ['top_k', 'min_p', 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs', 'reasoning_effort'] }, | |
| // Workers AI's OpenAI-compat endpoint parses a known subset; send only what | |
| // it understands. It also applies a small server-side max_tokens default | |
| // when the request omits one, so an SDK client that never sets max_tokens | |
| // (the common case) sees answers stop a couple of seconds in β reported on | |
| // @cf/openai/gpt-oss-120b, where the model's own reasoning trace eats the | |
| // default before any visible text lands (#553). 8192 is the floor we send | |
| // instead: generous for a chat turn plus reasoning, and small enough to fit | |
| // inside the smallest context window in the Workers AI catalog (24K on | |
| // @cf/meta/llama-3.3-70b-instruct-fp8-fast) with room left for the prompt. | |
| cloudflare: { | |
| drop: ['min_p', 'logit_bias', 'logprobs', 'top_logprobs', 'reasoning_effort'], | |
| defaultMaxTokens: 8192, | |
| }, | |
| // Radeon Cloud silently drops these fields. Keep reasoning_effort within the | |
| // common subset of its current shared roster (Qwen: low/medium; DeepSeek is | |
| // broader) so either model receives a supported value. | |
| radeon: { | |
| drop: ['top_k', 'min_p', 'seed', 'repetition_penalty', 'logit_bias', 'logprobs', 'top_logprobs'], | |
| reasoningEfforts: ['low', 'medium'], | |
| jsonSchemaToObject: true, | |
| }, | |
| // AI Horde builds its own payload format; none of the extended set maps. | |
| aihorde: { drop: [...EXTENDED_SAMPLING_KEYS] }, | |
| // Kilo's anonymous gateway 400s ("Provider returned error") whenever | |
| // response_format is present β observed live 2026-07-11; seed passes fine. | |
| // Dropping it also makes structured-output routing skip kilo entirely. | |
| kilo: { drop: ['response_format'] }, | |
| // Reka supports json_schema but rejects json_object (live 2026-07-11); | |
| // upgraded on the wire instead of dropped. | |
| reka: { jsonObjectToSchema: true }, | |
| }; | |
| // The permissive schema a json_object request is upgraded to on platforms | |
| // that only accept json_schema (any JSON object satisfies it). The empty | |
| // `properties` map matters: Reka 503s on a bare {type:'object'} but accepts | |
| // this shape (probed live 2026-07-11); additionalProperties keeps arbitrary | |
| // keys legal. | |
| const ANY_OBJECT_SCHEMA = { | |
| type: 'json_schema' as const, | |
| json_schema: { name: 'json_output', schema: { type: 'object', properties: {}, additionalProperties: true } }, | |
| }; | |
| /** | |
| * Build the extended wire-body fields for one platform: policy droplist | |
| * applied, renames applied, undefined skipped. Adapters spread the result | |
| * into their OpenAI-shaped request bodies. | |
| */ | |
| export function extendedBodyParams(platform: string, options: ExtendedSamplingOptions | undefined): Record<string, unknown> { | |
| if (!options) return {}; | |
| const policy = PLATFORM_PARAM_POLICIES[platform as Platform]; | |
| const dropped = new Set<string>(policy?.drop ?? []); | |
| const out: Record<string, unknown> = {}; | |
| for (const key of EXTENDED_SAMPLING_KEYS) { | |
| if (dropped.has(key)) continue; | |
| let value = (options as Record<string, unknown>)[key]; | |
| if (value === undefined) continue; | |
| if (key === 'response_format' && policy?.jsonObjectToSchema | |
| && (value as { type?: string }).type === 'json_object') { | |
| value = ANY_OBJECT_SCHEMA; | |
| } | |
| if (key === 'response_format' && policy?.jsonSchemaToObject | |
| && (value as { type?: string }).type === 'json_schema') { | |
| value = { type: 'json_object' }; | |
| } | |
| if (key === 'reasoning_effort' && policy?.reasoningEfforts) { | |
| value = clampEffortTo(value as ReasoningEffort, policy.reasoningEfforts); | |
| if (value === undefined) continue; | |
| } | |
| out[policy?.rename?.[key] ?? key] = value; | |
| } | |
| return out; | |
| } | |
| /** The output-token floor this platform sends for a request that carries no | |
| * max_tokens at all, or undefined when the provider's own default is fine. */ | |
| export function defaultMaxTokensFor(platform: string): number | undefined { | |
| return PLATFORM_PARAM_POLICIES[platform as Platform]?.defaultMaxTokens; | |
| } | |
| /** This platform's own output-token ceiling, or undefined when it accepts | |
| * whatever max_tokens the client asks for. */ | |
| export function maxTokensCapFor(platform: string): number | undefined { | |
| return PLATFORM_PARAM_POLICIES[platform as Platform]?.maxTokensCap; | |
| } | |
| /** | |
| * The max_tokens to put on the wire for one request: whatever the client asked | |
| * for, or the platform's floor when the client asked for nothing (#553), then | |
| * lowered to the tightest ceiling that applies β the platform's own | |
| * maxTokensCap, the operator's unified cap, or both. With neither in play | |
| * nothing is clamped β a client-set value passes through untouched in both | |
| * directions, and the gateway's own guardrails (token budget, routing reserve) | |
| * have already had their say by the time an adapter calls this. | |
| * | |
| * EVERY adapter must send max_tokens through here, or the cap is not unified: | |
| * openai-compat (and its subclasses), cloudflare, cohere, google and aihorde | |
| * all do. | |
| */ | |
| export function resolveMaxTokens(platform: string, requested: number | undefined, contextBudget?: number): number | undefined { | |
| const resolved = requested ?? defaultMaxTokensFor(platform); | |
| if (resolved == null) return resolved; | |
| // The tighter ceiling wins: a platform's hard reject applies even with the | |
| // operator cap off, and an operator cap below it applies everywhere. | |
| const caps = [unifiedMaxTokensCap(), maxTokensCapFor(platform), contextBudget].filter((c): c is number => c != null && c > 0); | |
| return caps.length === 0 ? resolved : Math.max(1, Math.min(resolved, ...caps)); | |
| } | |
| // ββ Unified output-token cap βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Optional operator-level ceiling on max_tokens for EVERY client. Aggressive | |
| // clients (Open WebUI sends max_tokens=65536 by default) 400 against free | |
| // models whose output limit is 32768 (CF qwen3-30b, zhipu glm), and without a | |
| // ceiling the same invalid value rides every fallback candidate β the chain | |
| // cannot rescue the request. The cap only LOWERS an | |
| // excessive value; a client value at or below it is untouched, and clients that | |
| // send nothing still get today's platform floor. 'off' (default) keeps the | |
| // historical pass-through behaviour. | |
| export const UNIFIED_MAX_TOKENS_SETTING = 'unified_max_tokens'; | |
| /** The ceiling 'auto' clamps to: the output limit of the largest common free | |
| * catalog models. */ | |
| export const UNIFIED_MAX_TOKENS_AUTO = 32768; | |
| /** The configured unified output cap, or null when disabled ('off'/unset). | |
| * 'auto' resolves to UNIFIED_MAX_TOKENS_AUTO; an explicit integer is used | |
| * verbatim; anything else is treated as disabled so a bad value can't 400 | |
| * requests. Reads the settings table on every call β cheap (better-sqlite3 | |
| * sync read) and picks up dashboard changes without a restart, mirroring | |
| * guardrails.ts. */ | |
| export function unifiedMaxTokensCap(): number | null { | |
| let raw: string | undefined; | |
| try { | |
| raw = getSetting(UNIFIED_MAX_TOKENS_SETTING); | |
| } catch { | |
| return null; // DB not ready β never throw on the proxy hot path | |
| } | |
| if (!raw) return null; | |
| const value = raw.trim().toLowerCase(); | |
| if (value === '' || value === 'off' || value === '0') return null; | |
| if (value === 'auto') return UNIFIED_MAX_TOKENS_AUTO; | |
| const n = Number(value); | |
| return Number.isInteger(n) && n > 0 ? n : null; | |
| } | |
| /** True when this platform's policy strips response_format before send β the | |
| * router uses it to skip such platforms for structured-output requests. */ | |
| export function platformDropsResponseFormat(platform: string): boolean { | |
| return PLATFORM_PARAM_POLICIES[platform as Platform]?.drop?.includes('response_format') ?? false; | |
| } | |
| /** The advertised parameter list for a model on `platform` β the base set | |
| * every surface supports, plus tools when the model does, minus the | |
| * platform's droplist. */ | |
| export function supportedParametersFor(platform: string, caps: { tools?: boolean } = {}): string[] { | |
| // Unlike the generic base set, Moondream has neither stop nor tools. | |
| if (platform === 'moondream') return ['temperature', 'top_p', 'max_tokens', 'max_completion_tokens', 'stream', 'reasoning_effort']; | |
| const policy = PLATFORM_PARAM_POLICIES[platform as Platform]; | |
| const dropped = new Set<string>(policy?.drop ?? []); | |
| const params = [ | |
| 'temperature', 'top_p', 'max_tokens', 'max_completion_tokens', 'stop', 'stream', | |
| ...EXTENDED_SAMPLING_KEYS.filter(k => !dropped.has(k)), | |
| ]; | |
| if (caps.tools) params.push('tools', 'tool_choice', 'parallel_tool_calls'); | |
| return params; | |
| } | |
| /** For a model served by several platforms (a unify group): the INTERSECTION | |
| * of the members' supported sets β a param is only advertised when every | |
| * platform the router might pick honors it. */ | |
| export function supportedParametersForPlatforms(platforms: string[], caps: { tools?: boolean } = {}): string[] { | |
| if (platforms.length === 0) return supportedParametersFor('', caps); | |
| const [first, ...rest] = platforms.map(p => supportedParametersFor(p, caps)); | |
| const restSets = rest.map(list => new Set(list)); | |
| return first.filter(param => restSets.every(s => s.has(param))); | |
| } | |