@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
590 lines (589 loc) • 24.7 kB
JavaScript
/**
* Context Window Registry
*
* Accurate per-provider, per-model context window sizes (INPUT token limits).
* These are distinct from OUTPUT token limits in tokens.ts.
*
* Sources:
* - Anthropic: https://docs.anthropic.com/en/docs/about-claude/models
* - OpenAI: https://platform.openai.com/docs/models
* - Google: https://ai.google.dev/gemini-api/docs/models
* - Others: Provider documentation as of Feb 2026
*/
import { DynamicModelProvider } from "../core/dynamicModels.js";
import { logger } from "../utils/logger.js";
import { resolveManifestEntryStrict } from "../models/manifestRegistry.js";
import { getCatalogJsonEntries } from "../providers/catalog/loader.js";
/**
* Per-provider context window blocks for the 9 JSON-catalog providers
* (cerebras, cloudflare, fireworks, groq, mistral, perplexity, sambanova,
* together-ai, xai), derived from models.catalog[*].contextWindow +
* defaultContextWindow — the catalog JSON (src/lib/providers/catalog/<id>.json)
* is their single source of truth. A catalog model with no recorded
* contextWindow (e.g. fireworks' current roster — none of its models have a
* sourced value yet) is simply omitted here, same as it was never a key in
* the pre-migration table either; the `_default` / prefix-match /
* DEFAULT_CONTEXT_WINDOW fallback chain in getContextWindowSize() below
* covers it exactly as it always has.
*/
const CATALOG_CONTEXT_WINDOWS = Object.fromEntries(getCatalogJsonEntries().map((entry) => [
entry.id,
{
_default: entry.models.defaultContextWindow,
...Object.fromEntries(Object.entries(entry.models.catalog).flatMap(([modelId, spec]) => spec.contextWindow !== undefined
? [[modelId, spec.contextWindow]]
: [])),
},
]));
/** Default context window when provider/model is unknown */
export const DEFAULT_CONTEXT_WINDOW = 128_000;
/** Maximum output reserve when maxTokens not specified */
export const MAX_DEFAULT_OUTPUT_RESERVE = 64_000;
/** Default output reserve ratio (35% of context) */
export const DEFAULT_OUTPUT_RESERVE_RATIO = 0.35;
/**
* Per-provider, per-model context window sizes.
* The "_default" key is the fallback for unknown models within a provider.
*/
export const MODEL_CONTEXT_WINDOWS = {
"nvidia-nim": {
_default: 128_000,
"meta/llama-3.3-70b-instruct": 128_000,
"meta/llama-3.1-405b-instruct": 128_000,
"meta/llama-3.1-70b-instruct": 128_000,
"meta/llama-3.2-90b-vision-instruct": 128_000,
"meta/llama-3.2-11b-vision-instruct": 128_000,
"nvidia/llama-3.3-nemotron-super-49b-v1": 128_000,
"nvidia/llama-3.1-nemotron-nano-8b-v1": 128_000,
"nvidia/llama-3.1-nemotron-70b-instruct": 128_000,
"deepseek-ai/deepseek-r1": 128_000,
"deepseek-ai/deepseek-r1-distill-llama-70b": 128_000,
"mistralai/mixtral-8x22b-instruct-v0.1": 65_536,
"mistralai/mixtral-8x7b-instruct-v0.1": 32_768,
"microsoft/phi-4": 16_384,
"google/gemma-3-27b-it": 8_192,
"openai/gpt-oss-20b": 131_072,
},
"lm-studio": {
_default: 8_192,
},
llamacpp: {
_default: 8_192,
},
cohere: {
_default: 128_000,
"command-r-plus": 128_000,
"command-r": 128_000,
"command-r7b-12-2024": 128_000,
},
replicate: {
// Per-model — Replicate hosts arbitrary models; sensible default.
_default: 32_768,
},
voyage: {
// Voyage embeddings: max input tokens vary 16K-32K per model
_default: 32_000,
"voyage-4": 32_000,
"voyage-4-large": 32_000,
"voyage-4-lite": 32_000,
"voyage-code-4": 32_000,
"voyage-3.5": 32_000,
"voyage-3.5-lite": 32_000,
"voyage-3-large": 32_000,
"voyage-code-3": 32_000,
"voyage-finance-2": 32_000,
"voyage-law-2": 16_000,
"voyage-multilingual-2": 32_000,
},
jina: {
// Jina embeddings: 8K input tokens for v3; 8K for v2; 32K for ColBERT-v2
_default: 8_192,
"jina-embeddings-v3": 8_192,
"jina-embeddings-v2-base-en": 8_192,
"jina-embeddings-v2-small-en": 8_192,
"jina-colbert-v2": 32_000,
},
stability: {
// Image-gen — context is prompt length only; ~2000 char limit
_default: 2_000,
},
ideogram: {
_default: 2_000,
},
recraft: {
_default: 2_000,
},
anthropic: {
_default: 200_000,
// Claude 5.5 / 5.1 (Sep 2026) — 1M context window, confirmed on
// platform.claude.com/docs/en/about-claude/models/overview
"claude-opus-5-5": 1_000_000,
"claude-sonnet-5-5": 1_000_000,
"claude-fable-5-1": 1_000_000,
// Claude 5 (mid 2026) — 1M context window
"claude-sonnet-5": 1_000_000,
"claude-opus-5": 1_000_000,
"claude-fable-5": 1_000_000,
// Claude 4.7 / 4.8 — 1M context window (platform.claude.com/docs/en/models)
"claude-opus-4-8": 1_000_000,
"claude-opus-4-7": 1_000_000,
// Claude 4.6 (Feb 2026) — 1M context window
"claude-opus-4-6": 1_000_000,
"claude-sonnet-4-6": 1_000_000,
// Claude 4.5
"claude-opus-4-5-20251101": 200_000,
"claude-sonnet-4-5-20250929": 200_000,
"claude-haiku-4-5-20251001": 200_000,
// Claude 4.x
"claude-opus-4-1-20250805": 200_000,
"claude-opus-4-20250514": 200_000,
"claude-sonnet-4-20250514": 200_000,
// Claude 3.x
"claude-3-7-sonnet-20250219": 200_000,
"claude-3-5-sonnet-20241022": 200_000,
"claude-3-5-haiku-20241022": 200_000,
"claude-3-opus-20240229": 200_000,
"claude-3-sonnet-20240229": 200_000,
"claude-3-haiku-20240307": 200_000,
},
openai: {
_default: 128_000,
// GPT-6 family (Sep 2026) — 1.05M context, per
// developers.openai.com/api/docs/models
"gpt-6-astra": 1_050_000,
"gpt-6-sol": 1_050_000,
"gpt-6-luna": 1_050_000,
// GPT-5.4 family — 1.05M context
"gpt-5.4": 1_050_000,
"gpt-5.4-mini": 400_000,
"gpt-5.4-nano": 400_000,
"gpt-5.4-pro": 1_050_000,
// GPT-5.x family — 400K context
"gpt-5.3-codex": 400_000,
"gpt-5.2": 400_000,
"gpt-5.2-pro": 400_000,
"gpt-5.2-codex": 400_000,
"gpt-5.2-chat-latest": 128_000,
"gpt-5.1": 400_000,
"gpt-5.1-codex": 400_000,
"gpt-5.1-codex-max": 400_000,
"gpt-5.1-codex-mini": 400_000,
"gpt-5.1-chat-latest": 128_000,
"gpt-5": 400_000,
"gpt-5-mini": 400_000,
"gpt-5-nano": 400_000,
"gpt-5-pro": 400_000,
"gpt-5-codex": 400_000,
"gpt-5-chat-latest": 128_000,
// GPT Open Source
"gpt-oss-120b": 128_000,
"gpt-oss-20b": 128_000,
// GPT-4.1 family — 1M context
"gpt-4.1": 1_047_576,
"gpt-4.1-mini": 1_047_576,
"gpt-4.1-nano": 1_047_576,
// GPT-4o
"gpt-4o": 128_000,
"gpt-4o-mini": 128_000,
// O-series reasoning — 200K context
o1: 200_000,
"o1-mini": 128_000,
"o1-pro": 200_000,
o3: 200_000,
"o3-mini": 200_000,
"o3-pro": 200_000,
"o4-mini": 200_000,
// Legacy
"gpt-4-turbo": 128_000,
"gpt-4": 8_192,
"gpt-3.5-turbo": 16_385,
},
"google-ai": {
_default: 1_048_576,
// Gemini 3.1 Series (all require -preview suffix)
"gemini-3.1-pro-preview": 1_048_576,
"gemini-3.1-flash-lite-preview": 1_048_576,
"gemini-3.1-flash-image-preview": 1_048_576,
"gemini-3.1-pro-preview-customtools": 1_048_576,
// Gemini 3 Series
"gemini-3-flash-preview": 1_048_576,
"gemini-3-pro-image-preview": 65_536,
/** @deprecated SHUT DOWN March 9, 2026. Migrate to gemini-3.1-pro-preview. */
"gemini-3-pro-preview": 1_048_576,
"gemini-2.5-pro": 1_048_576,
"gemini-2.5-flash": 1_048_576,
"gemini-2.5-flash-lite": 1_048_576,
"gemini-2.5-flash-image": 32_768,
"gemini-2.0-flash": 1_048_576,
"gemini-1.5-pro": 2_097_152,
"gemini-1.5-flash": 1_048_576,
},
vertex: {
_default: 1_048_576,
// Claude on Vertex
"claude-opus-5-5": 1_000_000,
"claude-sonnet-5-5": 1_000_000,
"claude-fable-5-1": 1_000_000,
"claude-sonnet-5": 1_000_000,
// Placed before the 200K `claude-opus-4` prefix key below: the lookup takes
// the first prefix that matches, so a dated `claude-opus-4-8@...` id would
// otherwise land on 200K.
"claude-opus-5": 1_000_000,
"claude-fable-5": 1_000_000,
"claude-opus-4-8": 1_000_000,
"claude-opus-4-7": 1_000_000,
"claude-opus-4-6": 1_000_000,
"claude-sonnet-4-6": 1_000_000,
"claude-sonnet-4-5": 200_000,
"claude-opus-4-5": 200_000,
"claude-haiku-4-5": 200_000,
"claude-sonnet-4": 200_000,
"claude-sonnet-4-20250514": 200_000,
"claude-opus-4-20250514": 200_000,
"claude-opus-4": 200_000,
// Catch-all for UNKNOWN Claude models on Vertex (prefix match runs after
// the specific keys above). Without this, an unlisted Claude model falls
// to the Gemini-shaped _default (1,048,576) — ABOVE Anthropic's real 1M
// API cap — so the pre-dispatch budget check and the in-loop context
// guard both under-guard it (the claude-sonnet-5 1,005,647-token 400s).
// 200K is the conservative Anthropic floor; list real models explicitly.
"claude-": 200_000,
// Gemini 3.1 on Vertex (all require -preview suffix)
"gemini-3.1-pro-preview": 1_048_576,
"gemini-3.1-flash-lite-preview": 1_048_576,
"gemini-3.1-flash-image-preview": 1_048_576,
"gemini-3.1-pro-preview-customtools": 1_048_576,
// Gemini 3 on Vertex
"gemini-3-flash-preview": 1_048_576,
"gemini-3-pro-image-preview": 65_536,
/** @deprecated SHUT DOWN March 9, 2026. Migrate to gemini-3.1-pro-preview. */
"gemini-3-pro-preview": 1_048_576,
// Gemini 2.x on Vertex
"gemini-2.5-pro": 1_048_576,
"gemini-2.5-flash": 1_048_576,
"gemini-2.0-flash": 1_048_576,
"gemini-1.5-pro": 2_097_152,
"gemini-1.5-flash": 1_048_576,
},
bedrock: {
_default: 200_000,
// Claude 5.5 / 5.1 (Sep 2026)
"anthropic.claude-opus-5-5": 1_000_000,
"anthropic.claude-sonnet-5-5": 1_000_000,
"anthropic.claude-fable-5-1": 1_000_000,
// Claude 4.6
"anthropic.claude-opus-4-6-v1": 1_000_000,
"anthropic.claude-sonnet-4-6": 1_000_000,
// Claude 4.5
"anthropic.claude-opus-4-5-20251101-v1:0": 200_000,
"anthropic.claude-sonnet-4-5-20250929-v1:0": 200_000,
"anthropic.claude-haiku-4-5-20251001-v1:0": 200_000,
// Claude legacy
"anthropic.claude-3-5-sonnet-20241022-v1:0": 200_000,
"anthropic.claude-3-5-haiku-20241022-v1:0": 200_000,
"anthropic.claude-3-opus-20240229-v1:0": 200_000,
"anthropic.claude-3-sonnet-20240229-v1:0": 200_000,
"anthropic.claude-3-haiku-20240307-v1:0": 200_000,
// Amazon Nova
"amazon.nova-pro-v1:0": 300_000,
"amazon.nova-lite-v1:0": 300_000,
"amazon.nova-2-lite-v1:0": 1_000_000,
// Writer
"writer.palmyra-x5-v1:0": 1_000_000,
"writer.palmyra-x4-v1:0": 128_000,
// NVIDIA
"nvidia.nemotron-nano-3-30b": 256_000,
},
azure: {
_default: 128_000,
// GPT-6 family (Sep 2026) — 1.05M context, per
// learn.microsoft.com/azure/ai-foundry/openai/concepts/models
"gpt-6-astra": 1_050_000,
"gpt-6-sol": 1_050_000,
"gpt-6-luna": 1_050_000,
// GPT-5.6 / GPT-5.5 — Azure-only as of Sep 2026, same 1.05M context
"gpt-5.6-sol": 1_050_000,
"gpt-5.6-terra": 1_050_000,
"gpt-5.6-luna": 1_050_000,
"gpt-5.5": 1_050_000,
// GPT-5.4
"gpt-5.4": 1_050_000,
"gpt-5.4-mini": 400_000,
"gpt-5.4-nano": 400_000,
"gpt-5.4-pro": 1_050_000,
// GPT-5.x
"gpt-5.2": 400_000,
"gpt-5.2-pro": 400_000,
"gpt-5.2-codex": 400_000,
"gpt-5.1": 400_000,
"gpt-5": 400_000,
"gpt-5-mini": 400_000,
// GPT-4.1
"gpt-4.1": 1_047_576,
"gpt-4.1-mini": 1_047_576,
// GPT-4o
"gpt-4o": 128_000,
"gpt-4o-mini": 128_000,
// O-series
o3: 200_000,
"o3-mini": 200_000,
"o4-mini": 200_000,
// Legacy
"gpt-4-turbo": 128_000,
"gpt-4": 8_192,
},
ollama: {
_default: 128_000,
},
litellm: {
_default: 128_000,
},
huggingface: {
_default: 32_000,
},
sagemaker: {
_default: 128_000,
// NVIDIA Nemotron 3 Nano (February 2026) — 1M context
"nvidia-nemotron-3-nano-30b": 1_000_000,
// Qwen3 VL — 32K context
"qwen3-vl-8b-instruct": 32_768,
},
...CATALOG_CONTEXT_WINDOWS,
};
/**
* Map of provider aliases to canonical MODEL_CONTEXT_WINDOWS keys.
*
* Callers reach `getContextWindowSize` via the unnormalized form on
* `options.provider` (e.g. CLI `--provider lmstudio`, alias `llama.cpp`),
* and `ProviderFactory.normalizeProviderName` runs only at instantiation —
* its output never reaches budget calculations. Without this normalization
* those alias forms miss the table and fall back to `DEFAULT_CONTEXT_WINDOW`,
* understating the budget for LM Studio / llama.cpp / NVIDIA NIM.
*
* The keys here are the result of stripping non-alpha characters, so
* `lm-studio` -> `lmstudio`, `nvidia-nim` -> `nvidianim`, `llama.cpp` -> `llamacpp`.
*/
const PROVIDER_ALIAS_MAP = {
// Both spellings resolve to `google-ai`, the ONLY key this table holds for
// AI Studio. `google-ai-studio` was never a key, so every realistic spelling
// ("googleAiStudio" from the native client, "google-ai-studio", "googleai")
// missed the table and fell back to DEFAULT_CONTEXT_WINDOW — reporting
// 128K for a 1,048,576-token Gemini. That understated the loop-guard budget
// by ~8x, so AI Studio agent loops reclaimed tool history that still fitted
// comfortably. `google-ai` itself only resolved via the raw-provider
// fallback below, which the normalized lookup now covers directly.
googleaistudio: "google-ai",
googleai: "google-ai",
lmstudio: "lm-studio",
llamacpp: "llamacpp",
nvidianim: "nvidia-nim",
nim: "nvidia-nim",
nvidia: "nvidia-nim",
deepseek: "deepseek",
};
function normalizeProviderForLookup(provider) {
const stripped = provider.toLowerCase().replace(/[^a-z]/g, "");
// On alias miss, return the *stripped* key — not the raw input — so case /
// separator variants ("OpenAI", "open-ai", "Vertex AI") still find their
// table entry under the lowercase canonical key instead of falling through
// to DEFAULT_CONTEXT_WINDOW.
return PROVIDER_ALIAS_MAP[stripped] ?? stripped;
}
/**
* Runtime-discovered context windows, keyed `${provider}:${model}`.
*
* Populated asynchronously by providers that can discover real per-model
* limits at runtime (e.g. the LiteLLM provider reads `max_input_tokens` from
* the proxy's `/model/info`), and read synchronously by
* {@link getContextWindowSize} — the same async-populate/sync-read contract as
* the DynamicModelProvider registry. Keys use the RAW provider string callers
* pass into budget calculations (see the alias-map comment above: normalized
* provider names never reach these lookups).
*/
const RUNTIME_CONTEXT_WINDOWS = new Map();
/**
* Register a runtime-discovered context window for a provider/model pair.
* Later registrations overwrite earlier ones (rediscovery refreshes values).
* Non-positive/non-finite windows are ignored so a malformed discovery source
* can never shrink a budget to zero.
*/
export function registerRuntimeContextWindow(provider, model, contextWindow) {
if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
return;
}
RUNTIME_CONTEXT_WINDOWS.set(`${provider}:${model}`, contextWindow);
}
/** Test hook: clear runtime-discovered windows (state is module-global). */
export function clearRuntimeContextWindows() {
RUNTIME_CONTEXT_WINDOWS.clear();
}
/**
* Runtime-discovered window for an exact provider/model pair, or undefined.
*
* Unlike {@link getContextWindowSize} this NEVER falls back to static table
* values. Callers use it to distinguish "the serving infrastructure told us
* the real window" (safe to hard-enforce: clamp max_tokens, fail fast) from
* "static guess" (advisory only — hard-enforcing a guessed window would
* falsely reject requests that the real deployment accepts).
*/
export function getRuntimeContextWindow(provider, model) {
if (!model) {
return undefined;
}
return RUNTIME_CONTEXT_WINDOWS.get(`${provider}:${model}`);
}
/**
* Runtime-discovered output-token ceilings, keyed `${provider}:${model}` —
* the `max_output_tokens` the serving infrastructure advertises for a model
* (e.g. LiteLLM `/model/info`). Same async-populate/sync-read contract as
* {@link registerRuntimeContextWindow}. Consumed by `getSafeMaxTokens` so
* requested maxTokens is clamped to the deployed model's real cap instead of
* a static per-provider table value.
*/
const RUNTIME_OUTPUT_CEILINGS = new Map();
/**
* Register a runtime-discovered output-token ceiling for a provider/model
* pair. Later registrations overwrite earlier ones (rediscovery refreshes
* values). Non-positive/non-finite ceilings are ignored so a malformed
* discovery source can never shrink an output budget to zero.
*/
export function registerRuntimeOutputCeiling(provider, model, maxOutputTokens) {
if (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) {
return;
}
RUNTIME_OUTPUT_CEILINGS.set(`${provider}:${model}`, maxOutputTokens);
}
/**
* Runtime-discovered output ceiling for an exact provider/model pair, or
* undefined when the serving infrastructure has not advertised one.
*/
export function getRuntimeOutputCeiling(provider, model) {
if (!model) {
return undefined;
}
return RUNTIME_OUTPUT_CEILINGS.get(`${provider}:${model}`);
}
/** Remove a model ceiling that discovery no longer advertises as valid. */
export function clearRuntimeOutputCeiling(provider, model) {
RUNTIME_OUTPUT_CEILINGS.delete(`${provider}:${model}`);
}
/** Test hook: clear runtime-discovered output ceilings (state is module-global). */
export function clearRuntimeOutputCeilings() {
RUNTIME_OUTPUT_CEILINGS.clear();
}
/**
* Resolve context window size for a provider/model combination.
*
* Priority:
* 0. Dynamic model registry (DynamicModelProvider) — resolves cross-provider
* models (e.g. Claude on Vertex) that the static table cannot handle
* 0.5 Runtime-discovered windows (registerRuntimeContextWindow) — real
* per-model limits fetched from the serving infrastructure (LiteLLM
* `/model/info`)
* 1. Manifest exact/alias match ONLY (resolveManifestEntryStrict — no prefix, see
* src/lib/models/manifestRegistry.ts) — the emerging single source of
* truth for per-model metadata. Deliberately consulted for a REAL match
* only (never the manifest's synthesized/generic default): most provider
* manifests are still "minimal" stubs holding nothing but a provider-wide
* default (e.g. vertex.ts, together-ai.ts) and none of the per-model
* overrides — including Vertex's Claude "claude-" prefix catch-all below,
* which exists specifically to stop an unlisted Claude-on-Vertex model
* from inheriting Gemini's 1,048,576 default — that still live only in
* MODEL_CONTEXT_WINDOWS. Falling back to a manifest-wide default here
* would silently drop that coverage. When a manifest DOES carry a real
* entry for this model, it wins even if it disagrees with the legacy
* table (e.g. Mistral's manifest supersedes the coarser, older
* MODEL_CONTEXT_WINDOWS.mistral values).
* 2. Exact model match under provider in static registry
* 3. Prefix match under provider in static registry
* 4. Provider's _default in static registry
* 5. Global DEFAULT_CONTEXT_WINDOW
*/
export function getContextWindowSize(provider, model) {
// Step 0: Check dynamic model registry first.
// This resolves cases where the runtime provider differs from the model's
// origin (e.g. Claude running via Vertex would hit Vertex's Gemini default
// in the static table). The dynamic registry knows the actual model metadata.
if (model) {
try {
const dynamicProvider = DynamicModelProvider.getInstance();
const modelConfig = dynamicProvider.resolveModel(provider, model);
if (modelConfig?.contextWindow) {
logger.debug(`[ContextWindow] Resolved via dynamic registry: provider=${provider}, model=${model}, contextWindow=${modelConfig.contextWindow}`);
return modelConfig.contextWindow;
}
}
catch {
// Dynamic registry not initialized yet — fall through to static lookup
}
}
// Step 0.5: Runtime-discovered window for this exact provider/model.
if (model) {
const discovered = RUNTIME_CONTEXT_WINDOWS.get(`${provider}:${model}`);
if (discovered !== undefined) {
return discovered;
}
}
// Static fallback chain — normalize aliases first so "lmstudio" / "llama.cpp" /
// "nvidianim" find their canonical entries instead of falling back to default.
const canonical = normalizeProviderForLookup(provider);
// Step 1: Manifest real-entry lookup (exact id, alias, or longest-prefix —
// see resolveManifestEntryStrict's docblock). Tries the alias-normalized
// provider key first, then the raw provider string, mirroring the
// `MODEL_CONTEXT_WINDOWS[canonical] ?? MODEL_CONTEXT_WINDOWS[provider]`
// double-lookup below — MANIFEST_REGISTRY is keyed by the same hyphenated
// AIProviderName forms (e.g. "together-ai") that normalizeProviderForLookup
// strips and PROVIDER_ALIAS_MAP doesn't cover, so callers passing the raw
// enum value still resolve. Only a genuine match short-circuits; a miss
// falls straight through to the untouched legacy cascade below.
if (model) {
const manifestEntry = resolveManifestEntryStrict(canonical, model) ??
resolveManifestEntryStrict(provider, model);
if (manifestEntry) {
return manifestEntry.contextWindow;
}
}
const providerWindows = MODEL_CONTEXT_WINDOWS[canonical] ?? MODEL_CONTEXT_WINDOWS[provider];
if (!providerWindows) {
return DEFAULT_CONTEXT_WINDOW;
}
if (model && providerWindows[model] !== undefined) {
return providerWindows[model];
}
// Try partial match (model name may be a prefix)
if (model) {
for (const [key, value] of Object.entries(providerWindows)) {
if (key !== "_default" && model.startsWith(key)) {
return value;
}
}
}
return providerWindows._default ?? DEFAULT_CONTEXT_WINDOW;
}
/**
* Calculate output token reserve for a given context window.
*
* Returns the *real* token count that will be reserved for output so callers
* (`getAvailableInputTokens`, `BudgetChecker`, conversation-memory pruning, file
* summarisation) compute input budget against the actual outgoing maxTokens.
*
* @param contextWindow - Total context window size
* @param maxTokens - Explicit maxTokens from user config (if set)
* @returns Number of tokens reserved for output (matches what's sent upstream)
*/
export function getOutputReserve(contextWindow, maxTokens) {
if (maxTokens !== undefined && maxTokens > 0) {
return maxTokens;
}
return Math.min(MAX_DEFAULT_OUTPUT_RESERVE, Math.ceil(contextWindow * DEFAULT_OUTPUT_RESERVE_RATIO));
}
/**
* Calculate available input tokens for a given provider/model.
*
* available = contextWindow - outputReserve
*/
export function getAvailableInputTokens(provider, model, maxTokens) {
const contextWindow = getContextWindowSize(provider, model);
const outputReserve = getOutputReserve(contextWindow, maxTokens);
return contextWindow - outputReserve;
}