UNPKG

@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
/** * 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; }