/** * Per-request resolution of a model's output-token ceiling. * * The wire adapters used to write `req.maxTokens ?? ctx.capabilities.maxOutput ?? 8192`, * which quietly collapsed to the 8192 literal in every situation where * `ctx.capabilities` was not the catalog-resolved capabilities for the model * actually being requested: * * - `withCatalogCapabilities` runs ONCE at boot, for `config.model`. Every * runtime provider rebuild (`/model` switch, fallback chain, session * resume, fleet host provider, WebUI switch) constructs a fresh Provider * carrying only the family baseline — where `maxOutput` is undefined. * - A single Provider instance serves requests for more than one model * (subagents on a model-matrix entry, the fallback chain), so a * provider-scoped `maxOutput` is structurally the wrong granularity. * * The result was that a model advertising `limit.output: 64000` on models.dev * got `max_tokens: 8192` on the wire — responses truncated at 1/8th of the * model's real ceiling. * * This module keys the lookup on `req.model`, which is always the model being * requested, and reads the same models.dev facts (`limit.output`) that the * catalog overlay reads. The resolver is installed once at boot by whichever * host owns the ModelsRegistry (CLI wiring, WebUI server); when it is absent * — plugin-built providers, unit tests, catalog unreachable — resolution falls * through to the provider capabilities and finally to the documented literal. */ import type { Capabilities, Config, ModelsRegistry, Request } from '@wrongstack/core/types'; /** * The ONLY hardcoded output number left in the codebase, and it exists solely * because Anthropic's Messages API makes `max_tokens` a REQUIRED field: with no * catalog entry and no capability overlay there is nothing to derive from, and * a request without the field is rejected outright. * * Every other wire format treats the cap as optional, so they omit it instead * (see `resolveMaxOutputTokens` returning undefined) and let the backend apply * the model's own ceiling — the correct number, sourced from the provider * rather than guessed here. * * Do NOT reuse this as a general default. */ export declare const REQUIRED_FIELD_LAST_RESORT_MAX_OUTPUT = 8192; /** Context handed to `buildBody`, carrying everything needed to size a response. */ export interface BuildBodyContext { capabilities: Capabilities; /** * The Provider's user-visible id. Optional so hand-written adapters and * tests that call `buildBody` directly keep compiling; when absent the * catalog lookup is skipped and capability/fallback resolution applies. */ providerId?: string | undefined; } export type ModelOutputLimitResolver = (providerId: string | undefined, modelId: string) => number | undefined; /** * Install the process-wide catalog resolver. Hosts normally call * {@link installCatalogModelOutputLimits} instead; this is the seam for tests * and for embedders with their own catalog. */ export declare function setModelOutputLimitResolver(resolver: ModelOutputLimitResolver | undefined): void; /** Drop the installed resolver. Tests use this to restore global state. */ export declare function clearModelOutputLimitResolver(): void; /** * The model's advertised output ceiling, or undefined when no resolver is * installed / the model is unknown. Never throws: a resolver that blows up * must not take down request building. */ export declare function resolveCatalogMaxOutput(providerId: string | undefined, modelId: string): number | undefined; /** * The output-token cap for one request, highest-priority source first: * * 1. `req.maxTokens` — the caller asked for a specific size * (one-shot LLM callers, compaction helpers, * the brain's configured budget). * 2. catalog, keyed on `req.model` — models.dev `limit.output`, plus the * user's `customModels` / per-provider * overrides from providers.json. Correct even * when the provider instance was built for * another model. * 3. `ctx.capabilities.maxOutput` — the boot-time overlay; still right for * the common single-model session, and the * only source when no resolver is installed. * * Returns undefined when none of them knows. Callers on a wire format where * the field is optional MUST omit it in that case rather than substitute a * number: the backend's own default is the model's real ceiling, whereas any * literal we invent here is a guess that silently truncates responses (this is * exactly how a fixed 8192 came to cap models with a 384_000 ceiling). */ export declare function resolveMaxOutputTokens(req: Pick, ctx: BuildBodyContext): number | undefined; /** * Same resolution, for wire formats that REQUIRE the field (Anthropic * Messages). Falls back to {@link REQUIRED_FIELD_LAST_RESORT_MAX_OUTPUT} only * when nothing is known, because omitting it would make the request invalid. */ export declare function resolveRequiredMaxOutputTokens(req: Pick, ctx: BuildBodyContext): number; export interface InstallCatalogOutputLimitsOptions { registry: ModelsRegistry; /** * Live config reader. Re-read on every lookup so a `/models add --max-output` * or a hot-reloaded provider config takes effect without a restart. */ getConfig?: (() => Config | undefined) | undefined; log?: ((message: string) => void) | undefined; } /** * Build the catalog index from the registry and install it as the process-wide * output-limit resolver. Safe to call more than once — the last install wins. * * Resolves after the first index build so boot can guarantee the very first * request already sees real limits. A catalog that is unreachable leaves the * resolver installed with an empty index: config overrides still apply and * everything else falls through to the capability overlay. */ export declare function installCatalogModelOutputLimits(opts: InstallCatalogOutputLimitsOptions): Promise; //# sourceMappingURL=model-output-limits.d.ts.map