import { type CatalogOptions, type ProviderCatalog } from '#src/providers/modelCatalog.js'; import type { ProviderId } from '#src/providers/modelDiscovery.js'; /** * GS2-59 — default context window (`num_ctx`) for Ollama models. Ollama's OWN default is 4096, but * gaunt-sloth's agentic prompt (system + full lean toolset + a tool result) already lands ~4000 * tokens; at 4096 a thinking model (e.g. gemma4:31b) spends its entire remaining budget on the * reasoning field and emits EMPTY `content` on the turn after a tool executes — the GS2-59 * blank-answer regression. The OpenAI-compat `/v1` shim IGNORES `num_ctx`; the native `/api/chat` * path honors it. * * 16384 is chosen as the largest window that is BOTH safely above the ~4000-token starvation point * (4× headroom for reasoning + a few tool results) AND fits constrained consumer VRAM: Ollama * preallocates the KV cache at `num_ctx`, so on a box where a large model already spills partly to * CPU (e.g. a 19GB model on ~18GB of GPU), a 32768 cache tips the GPU allocation into an * out-of-memory error. 16384 was verified live to run the agentic tool→synthesis turn on such a * box; 32768 OOM'd it. Overridable per config via `llm.numCtx` — raise it if you have the VRAM and * run long sessions, lower it on very tight hardware. NOTE: a per-request `num_ctx` overrides the * daemon's `OLLAMA_CONTEXT_LENGTH`, so a user who tuned their server window higher should set * `llm.numCtx` to match rather than rely on the server default. * * It lives here rather than in the provider module because two things now need it — the client the * provider builds and the guard that has to know what that client will send — and a second copy is * how the guard would come to reason about a window the request does not use. */ export declare const DEFAULT_OLLAMA_NUM_CTX = 16384; /** * The model's context window in tokens, or `null` when it is not known. * * Asynchronous because a source may have to ask the provider. Called on every model call, so an * implementation that does I/O is expected to memoise; {@link resolveContextWindowSource} does. */ export type ContextWindowSource = () => Promise; /** A source that never knows — the honest answer for every provider no source is wired for. */ export declare const UNKNOWN_CONTEXT_WINDOW: ContextWindowSource; /** * The fields the window resolver reads off a chat model, without depending on its class. * * Structural rather than a `ChatOllama` import on purpose: the provider module loads * `@langchain/ollama` dynamically so a session that never uses ollama never pays for it, and typing * against the class here would undo that. */ export interface OllamaLikeModel { _llmType?: () => string; numCtx?: number; model?: string; baseUrl?: string; } /** * **The ollama window: what we send, capped by what the model can hold.** * * The effective window is `numCtx` — the number gaunt-sloth puts on every native `/api/chat` * request, which overrides the daemon's own `OLLAMA_CONTEXT_LENGTH` — and never the daemon default, * because the request is what decides. It is then capped by the model's own `context_length` from * `/api/show`: asking for more than the model has does not buy more room, so guarding against the * larger number would let exactly the truncation this guard exists to prevent happen anyway. * * **Fail-soft in one direction only.** No daemon, a slow daemon, an unreadable answer — the * configured number stands, because that is still the number the request will carry. Nothing here * throws, and nothing here degrades a known window to unknown. */ export declare function createOllamaContextWindowSource(llm: OllamaLikeModel): ContextWindowSource; /** Where a resolved context window came from — carried so `/status` can say, and a wrong one is * diagnosable instead of mysterious. */ export type ContextWindowOrigin = 'ollama' | 'models.dev' | 'profile' | 'unknown'; /** * [[EXT-187]] — **whether anything was in a position to contradict this resolution.** * * `origin` says which tier produced the number. It cannot say whether that tier answered against * competition or alone, and on the profile tier those are materially different facts: the same * `origin: 'profile'` covers a table a live catalog was read alongside and a table that decided * unchallenged because no catalog slice was cached. [[EXT-185]] already computed that difference * inside {@link resolveContextWindow} and spent it on one `debugLog`; this is the same fact carried * to the caller, so `/status` and `/autocompact` can say which they are looking at. * * **Why a field beside `origin` rather than a fourth origin value.** A `'profile-unchecked'` member * would not add a state, it would *redefine an existing one*: every `origin === 'profile'` test — * ours and any embedder's, since `ContextWindowOrigin` is re-exported from the package barrel — * would silently stop matching the cold-cache case, which is the very case this node exists to make * visible. Widening the union is also a breaking change to a consumer switching exhaustively on it. * The two facts are orthogonal — *which source* and *was it checked* — and modelling them as one * value is how a reader comes to believe `origin` answers a question it does not. * * **Why three values and not a boolean.** A boolean would collapse "no catalog is cached" with "no * catalog exists for this provider", and only the first has a remedy. Telling a user on ollama — or * on any provider models.dev carries no slice for — to refresh a catalog that will never hold their * model is a diagnostic that lies, which is the distinction [[EXT-185]] drew deliberately at the * tier-2 seam and this type is not permitted to undo. * * The field describes the **resolution**, not only the number, so it is meaningful on an `unknown` * reading too: an empty resolution reached over a cold cache is one a cached catalog might have * answered, and that user has somewhere to go. */ export type ContextWindowCheck = /** * The number came from a source in a position to know it, or from a table a live catalog slice * was read alongside and did not contradict. * * The ollama tier counts as knowing: it does not *claim* a window, it reports the one this * session imposes — `numCtx` is the number the request will carry, capped by the model's own * `context_length` read from the daemon, so there is nothing for a catalog to contradict. * **Knowingly not modelled here:** a `/api/show` that failed leaves that cap unread, and the * configured number then stands uncapped. Reporting it would need {@link ContextWindowSource} to * carry more than `number | null` — a new contract for every source — to describe a narrower risk * than the one this type is about, since ollama truncates rather than errors and `numCtx` is * still the number sent. */ 'checked' /** * A compiled-in profile table decided alone: no models.dev slice is cached for this provider and * the runtime reads the catalog cache-only. **The state with a remedy** — models.dev does carry * this provider, so filling the cache puts a checker back in front of the table. */ | 'unchecked' /** * Nothing checked it and nothing here can: models.dev has no slice for this provider at all, or * the caller named no provider to look one up for. Distinct from `'unchecked'` precisely because * there is no remedy to offer. */ | 'uncheckable'; /** A resolved window and its provenance. `tokens: null` means "not known", and never "zero". */ export interface ContextWindowReading { tokens: number | null; origin: ContextWindowOrigin; /** [[EXT-187]] — whether anything was in a position to contradict this resolution. */ check: ContextWindowCheck; } /** How each origin is described to a user, in a sentence that says where to go to change it. */ export declare const CONTEXT_WINDOW_ORIGIN_LABELS: Readonly>; /** * [[EXT-187]] — what a surface says about each check state, or `null` for the states it says * nothing about. * * **Only `'unchecked'` speaks, and the two `null`s are a ruling rather than an omission.** A * warning is worth its space when the reader can act on it; `'uncheckable'` has no remedy to name, * so a line for it would be a permanent sentence on every ollama session telling the user something * they cannot change — the default-on noise [[EXT-168]] declined a notice over, and a line that * suggested refreshing a catalog there would be actively wrong. `'checked'` is the ordinary case * and needs no commentary. * * A total `Record` rather than a lookup with a fallback, so a fourth check state cannot be added * without someone deciding on the spot what it tells a user. */ export declare const CONTEXT_WINDOW_CHECK_LABELS: Readonly>; /** * The model's window as the LangChain provider package declares it: `llm.profile.maxInputTokens`. * * A getter on `BaseLanguageModel` that the base class answers with `{}` and each provider package * overrides, so an id its table has never heard of yields `undefined` rather than an error — * measured: `gpt-4o-mini` gives 128000 and `mistralai/mistral-7b-instruct` gives nothing. Wrapped * in a `try` because it is a getter on someone else's object and a throw here would take down a * resolution that has a perfectly good answer to fall back to. */ export declare function readProfileContextWindow(llm: unknown): number | null; /** What {@link resolveContextWindow} needs beyond the model itself — all of it injectable. */ export interface ContextWindowResolutionOptions { /** * The gth provider namespace (`anthropic`, `google-genai`, …) — `config.modelProviderType`, NOT * the model class's `_llmType()`. The two disagree exactly where it matters: `huggingface` * reports `openai`, and both Gemini providers report `google`, so keying the catalog on the * class's own label would read the wrong provider's slice or none at all. */ providerId?: string; /** The model id as models.dev keys it — `llm.model`, e.g. `claude-sonnet-4-5`. */ modelId?: string; /** * Options threaded to {@link getProviderCatalog} (cache dir, TTL, fetch impl) for hermetic tests. * * **`cacheOnly` defaults to `true` here and nowhere else.** This resolution sits in front of the * first model call of a session, and what a cold `api.json` fetch costs there is **bounded only * by the catalog fetch timeout**: a few hundred milliseconds on a fast link, and on a slow one * whatever the link takes, up to the timeout — a delay the user would experience as the agent * hanging before it said anything, to decide a threshold that already has a fallback. It is the * tail rather than the median that this option is protecting, since the cache has a free warm * path either way. `gth init` passes `cacheOnly: false` because it is an explicit, interactive * step that can afford to wait, and filling the cache there is what makes the runtime read a hit. * * The cost of *not* fetching is that the LangChain profile decides alone — see the ruling on that * branch in {@link resolveContextWindow}. */ catalogOptions?: CatalogOptions; /** Catalog reader override; {@link getProviderCatalog} when omitted. Injected by the tests. */ catalogReader?: (_providerId: ProviderId, _options: CatalogOptions) => Promise; /** Profile reader override; {@link readProfileContextWindow} when omitted. Injected by the tests. */ profileReader?: (_llm: unknown) => number | null; } /** * One resolution, read two ways: {@link ResolvedContextWindow.source} for the guard, which wants * only the number, and {@link ResolvedContextWindow.read} for `/status`, which also wants the * provenance. * * They are handed out as a pair from a single factory call, over a single memoised promise, so the * number `/status` prints is by construction the number the guard enforced. Two independent * resolvers could disagree — a stale catalog on one and a fresh fetch on the other — and a * `/status` that describes a threshold nobody is using is worse than no `/status` line at all. */ export interface ResolvedContextWindow { /** The window in tokens, or `null` for unknown. Memoised; safe to call before every model call. */ source: ContextWindowSource; /** The same resolution with its provenance attached. Shares the memoised promise. */ read: () => Promise; } /** * **The one place a model is matched to a context window.** Tries ollama, then models.dev, then the * LangChain profile, and answers `origin: 'unknown'` with `tokens: null` when none of them knows. * * Every answer also carries a {@link ContextWindowCheck} saying whether anything was in a position * to contradict it ([[EXT-187]]) — the fact that separates a profile table a catalog stood beside * from one that decided unchallenged. * * **There is deliberately no `?? DEFAULT` anywhere on this path.** One fallback turns every unknown * window into a confident wrong number, which is the 4097 failure this file opens by naming. * * The whole resolution is memoised as a PROMISE, for the reason the ollama source already gives: * this runs before every model call, the answer cannot change during a session, and holding the * promise rather than the value means concurrent calls share one catalog read instead of racing to * make several. */ export declare function resolveContextWindow(llm: unknown, options?: ContextWindowResolutionOptions): ResolvedContextWindow; /** * The window as a bare number, for a caller that does not need the provenance. * * A thin wrapper over {@link resolveContextWindow} rather than a second implementation, so the two * cannot answer differently. With no options it consults only the sources that need no * configuration — ollama and the profile — which is why a plain object with neither still answers * `null`. */ export declare function resolveContextWindowSource(llm: unknown, options?: ContextWindowResolutionOptions): ContextWindowSource;