@orkestrel/ollama 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#model","#url","#keepAlive","#timeout","#think","#options","#transport","#headers","#format","#fetch","#parseBody","#content","#thought","#thinking","#result","#tools","#usage","#deltas","#requestHeaders","#body","#plain","#arguments"],"sources":["../../../src/server/constants.ts","../../../src/server/errors.ts","../../../src/server/OllamaProvider.ts","../../../src/server/factories.ts"],"sourcesContent":["// Ollama constants — the provider's defaults (AGENTS §5).\n\n/** The local Ollama daemon base URL assumed when `OllamaOptions.url` is omitted. */\nexport const DEFAULT_OLLAMA_URL = 'http://localhost:11434'\n\n/**\n * How long the model stays resident after a call when `OllamaOptions.keepAlive` is\n * omitted — Ollama's own `keep_alive` default, expressed as a duration string.\n */\nexport const DEFAULT_KEEP_ALIVE = '5m'\n\n/**\n * The per-call deadline in milliseconds when `OllamaOptions.timeout` is omitted —\n * generous enough that a cold model load does not trip it.\n */\nexport const DEFAULT_PROVIDER_TIMEOUT = 120_000\n\n/**\n * The cap, in characters, on how much of a non-OK response body is\n * incorporated into a thrown {@link OllamaHTTPError}'s message.\n *\n * @remarks\n * Bounds the excerpt so a defensive proxy or a misbehaving daemon handing\n * back an unbounded response body cannot inflate the thrown error's message\n * without limit (§14). `2048` characters is generous enough to carry a\n * useful diagnostic snippet while staying well short of any practical size\n * concern.\n */\nexport const MAX_ERROR_BODY_LENGTH = 2048\n","// Errors for the Ollama provider. A single `OllamaHTTPError` carries the\n// `/api/chat` HTTP status at the boundary — non-OK responses and a missing\n// response body both throw it — so a `catch` can branch on `error.status`\n// rather than parsing a message (AGENTS §12).\n\n/**\n * An error thrown when the Ollama `/api/chat` HTTP transport fails.\n *\n * @remarks\n * Carries the response `status` (0 when no HTTP response was received at all,\n * e.g. a `null` body). Thrown by {@link OllamaProvider} at its two HTTP\n * failure sites — the non-OK status branch and the null-body branch — so a\n * caller can branch on `error.status` instead of parsing the message. Narrow\n * a caught value with {@link isOllamaHTTPError}.\n *\n * @example\n * ```ts\n * try {\n * \tawait provider.generate(messages, signal)\n * } catch (error) {\n * \tif (isOllamaHTTPError(error) && error.status === 404) {\n * \t\t// the configured model isn't pulled\n * \t}\n * }\n * ```\n */\nexport class OllamaHTTPError extends Error {\n\treadonly status: number\n\n\tconstructor(message: string, status: number, options?: { readonly cause?: unknown }) {\n\t\tsuper(message, options)\n\t\tthis.name = 'OllamaHTTPError'\n\t\tthis.status = status\n\t}\n}\n\n/**\n * Whether a value is an {@link OllamaHTTPError}.\n *\n * @param value - The value to test\n * @returns `true` when `value` is an `OllamaHTTPError`\n */\nexport function isOllamaHTTPError(value: unknown): value is OllamaHTTPError {\n\treturn value instanceof OllamaHTTPError\n}\n","import type {\n\tContextFormatInterface,\n\tMessageInterface,\n\tProviderDelta,\n\tProviderInterface,\n\tProviderResult,\n\tProviderStreamOptions,\n\tThinkSplitterInterface,\n} from '@orkestrel/agent'\nimport type { TokenUsage } from '@orkestrel/budget'\nimport type { ToolCall, ToolDefinition } from '@orkestrel/tool'\nimport type { OllamaOptions, OllamaResponse, WireChatRequest } from './types.js'\nimport { createThinkSplitter, ProviderAbortError } from '@orkestrel/agent'\nimport { isNumber, isRecord, isString } from '@orkestrel/contract'\nimport { createNDJSONParser } from '@orkestrel/ndjson'\nimport { Timeout } from '@orkestrel/timeout'\nimport {\n\tDEFAULT_KEEP_ALIVE,\n\tDEFAULT_OLLAMA_URL,\n\tDEFAULT_PROVIDER_TIMEOUT,\n\tMAX_ERROR_BODY_LENGTH,\n} from './constants.js'\nimport { OllamaHTTPError } from './errors.js'\n\n/**\n * The local Ollama inference boundary — a {@link ProviderInterface} over Ollama's\n * `POST /api/chat`, both non-streaming (`generate`) and streaming NDJSON (`stream`).\n *\n * @remarks\n * - **Wire protocol.** Posts `{ model, messages, stream, keep_alive, think }` plus\n * passthrough sampling `options` and mapped function `tools`. The `think` flag is\n * CONFIGURABLE via {@link OllamaOptions.think} (default `false`). Non-stream parses\n * one JSON body; stream consumes NDJSON (one JSON object per `\\n`-terminated line) —\n * deltas carry `message.content`, the final `done: true` line carries the token usage.\n * - **Think separation (H4).** The wire `think` flag is configurable\n * ({@link OllamaOptions.think}, default `false`). With `think: true` a thinking model's\n * daemon separates reasoning NATIVELY — returning it on the distinct `message.thinking`\n * channel (read here via `#thinking`) instead of inline in `message.content`. EITHER\n * way the per-call {@link ThinkSplitterInterface} is the defensive guarantee: a daemon\n * may ignore `think: false` for a thinking model and inline `<think>` tags, so every\n * content delta routes through the splitter, only CLEAN content is yielded / assembled,\n * and the separated reasoning (plus any daemon-side `message.thinking` deltas) lands on\n * `ProviderResult.thinking`, never in the conversation.\n * - **Boundary narrowing (§14).** Every wire value arrives as `unknown` and is\n * narrowed through guards (`isRecord` / `isString` / `isNumber`) — never `as`. A\n * missing / malformed field degrades to a sensible default (empty content, no\n * usage, `{}` arguments), never a throw.\n * - **Bounded.** Each call arms a {@link Timeout} for `OllamaOptions.timeout` and\n * passes `AbortSignal.any([timeout.signal, signal])` to `fetch`, so the caller's\n * signal AND the deadline both cancel the request. The timeout is always cleared —\n * in `#fetch` if the request fails/aborts, otherwise in the consuming call's `finally`.\n * - **Abort recovers partial.** A `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result assembled so far; pairing the\n * `TextDecoder({ stream: true })` with the {@link NDJSONParser} parser keeps multi-byte\n * UTF-8 splits and partial lines honest.\n * - **Event-free.** A pure functional boundary — no Emitter, no events.\n * - **Transport seam.** {@link OllamaOptions.fetch} swaps the transport (default\n * `globalThis.fetch`) and {@link OllamaOptions.headers} is a per-request, possibly\n * async header injector merged over the base `Content-Type` — so a browser runtime\n * can route through the developer's own server with an obfuscated bearer token,\n * without this library ever handling a real API key. Both omitted ⇒ today's behaviour.\n * Orthogonal to the deadline: the hook is awaited inside `#fetch`'s try, so a hook\n * rejection clears the armed timer like any other request failure.\n *\n * @example\n * ```ts\n * const provider = new OllamaProvider({ model: 'qwen3.5:2b-q4_K_M' })\n * const result = await provider.generate(messages, abort.signal)\n * ```\n */\nexport class OllamaProvider implements ProviderInterface {\n\treadonly id = crypto.randomUUID()\n\treadonly name = 'ollama'\n\treadonly #model: string\n\treadonly #url: string\n\treadonly #keepAlive: string | number\n\treadonly #timeout: number\n\treadonly #think: boolean\n\treadonly #options: Readonly<Record<string, unknown>> | undefined\n\treadonly #transport: typeof globalThis.fetch\n\treadonly #headers: (() => Record<string, string> | Promise<Record<string, string>>) | undefined\n\treadonly #format: ContextFormatInterface | undefined\n\n\tconstructor(options: OllamaOptions) {\n\t\tthis.#model = options.model\n\t\tthis.#url = options.url ?? DEFAULT_OLLAMA_URL\n\t\tthis.#keepAlive = options.keepAlive ?? DEFAULT_KEEP_ALIVE\n\t\tthis.#timeout = options.timeout ?? DEFAULT_PROVIDER_TIMEOUT\n\t\t// The `/api/chat` `think` wire flag — DEFAULT `false` so the general-purpose provider\n\t\t// stays backward-compatible and immediate for non-thinking models. A thinking model whose\n\t\t// reasoning is DISPLAYED separately sets `think: true`, and the daemon then returns it on\n\t\t// the `message.thinking` channel (`#thinking`) rather than inline in `message.content`.\n\t\tthis.#think = options.think ?? false\n\t\tthis.#options = options.options\n\t\t// The transport seam (§21): a custom fetch (defaulting to the global, BOUND to its\n\t\t// `globalThis` receiver — invoking a bare reference through a field loses the `window`\n\t\t// receiver and browsers throw `Illegal invocation`; node's fetch is receiver-agnostic,\n\t\t// so only a browser runtime ever saw it) and a dynamic header injector — both omitted\n\t\t// by default, so today's behaviour is byte-identical (the global fetch, only the JSON\n\t\t// content type). The injected transport is `#transport` (the request METHOD already\n\t\t// owns the `#fetch` name).\n\t\tthis.#transport = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#headers = options.headers\n\t\t// The context-framing default (the provider-DEFAULT level of AgentContext's format\n\t\t// cascade) — EXPOSE-ONLY: read by the Agent via `build(this.#provider.format)` and\n\t\t// consumed by core's cascade, it NEVER enters `#body` / the `/api/chat` wire. It is\n\t\t// NOT Ollama's structured-output `format` wire param — that one IS sent in `#body`,\n\t\t// but only when a per-call `ProviderStreamOptions.schema` is supplied; the two\n\t\t// merely share a word. Omitted ⇒ undefined ⇒ core's built-in framing.\n\t\tthis.#format = options.format\n\t}\n\n\t/**\n\t * The provider's context-framing default — the PROVIDER-DEFAULT level of\n\t * {@link import('@orkestrel/agent').AgentContextInterface.build}'s format cascade (it BEATS\n\t * the managers' built-in framing, is BEATEN by a manager-options or per-item override).\n\t * Satisfies the OPTIONAL {@link ProviderInterface.format} contract member: `undefined`\n\t * when {@link OllamaOptions.format} was omitted (the framing-agnostic default ⇒ core's\n\t * built-in framing applies unchanged), else the exact configured framing the Agent\n\t * threads into `build()`.\n\t *\n\t * @remarks\n\t * EXPOSE-ONLY — read by the Agent loop and consumed by core's cascade; it is NEVER sent\n\t * on the `/api/chat` wire (it is absent from `#body` / the request). This is NOT Ollama's\n\t * structured-output `format` wire parameter — that one IS sent in `#body`, but only when\n\t * a per-call `ProviderStreamOptions.schema` is supplied; only the word collides.\n\t *\n\t * @returns The configured {@link ContextFormatInterface}, or `undefined` when none\n\t */\n\tget format(): ContextFormatInterface | undefined {\n\t\treturn this.#format\n\t}\n\n\tasync generate(\n\t\tmessages: readonly MessageInterface[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<ProviderResult> {\n\t\tconst { response, timeout } = await this.#fetch(messages, false, signal, tools, options)\n\t\ttry {\n\t\t\tconst record = await this.#parseBody(response)\n\t\t\t// The one-body call routes through the SAME splitter as the stream (the daemon may\n\t\t\t// ignore `think: false` — the splitter is the guarantee): the assembled content is\n\t\t\t// CLEAN (the splitter's authoritative `content`, which also covers the qwen3\n\t\t\t// template's IMPLICIT leading open), the separated spans + any wire-side\n\t\t\t// `message.thinking` land on `thinking`.\n\t\t\tconst splitter = createThinkSplitter()\n\t\t\tsplitter.split(this.#content(record))\n\t\t\tsplitter.flush()\n\t\t\tconst thinking = this.#thought(splitter, this.#thinking(record))\n\t\t\treturn this.#result(splitter.content, thinking, this.#tools(record), this.#usage(record))\n\t\t} finally {\n\t\t\ttimeout.clear()\n\t\t}\n\t}\n\n\tasync *stream(\n\t\tmessages: readonly MessageInterface[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): AsyncGenerator<ProviderDelta, ProviderResult> {\n\t\tconst { response, timeout, combined } = await this.#fetch(\n\t\t\tmessages,\n\t\t\ttrue,\n\t\t\tsignal,\n\t\t\ttools,\n\t\t\toptions,\n\t\t)\n\t\tconst body = response.body\n\t\tif (body === null) {\n\t\t\ttimeout.clear()\n\t\t\tthrow new OllamaHTTPError('Ollama API error: no response body', 0)\n\t\t}\n\t\tconst reader = body.getReader()\n\t\tconst decoder = new TextDecoder()\n\t\tconst parser = createNDJSONParser()\n\t\t// The per-call think separator (H4): every wire content delta routes through it, so\n\t\t// only CLEAN content is yielded / assembled even when the daemon ignores `think: false`\n\t\t// for a thinking model; daemon-side `message.thinking` deltas accumulate beside it.\n\t\t// The ASSEMBLED content is the splitter's authoritative `content` — across the qwen3\n\t\t// template's IMPLICIT leading open (a bare `</think>` with the open pre-seeded into the\n\t\t// prompt scaffold) the splitter RECLASSIFIES the already-yielded prefix into `thinking`,\n\t\t// so the result stays clean even though those deltas could not be recalled.\n\t\tconst splitter = createThinkSplitter()\n\t\t// Mutable per-stream accumulator shared between the live loop and the post-loop\n\t\t// NDJSON tail flush below — the SAME object reference threads through every\n\t\t// `#deltas` call so `wired` / `calls` / `usage` accumulate across both sites.\n\t\tconst state: {\n\t\t\tsplitter: ThinkSplitterInterface\n\t\t\twired: string\n\t\t\tcalls: ToolCall[]\n\t\t\tusage: TokenUsage | undefined\n\t\t} = {\n\t\t\tsplitter,\n\t\t\twired: '',\n\t\t\tcalls: [],\n\t\t\tusage: undefined,\n\t\t}\n\t\ttry {\n\t\t\tfor (;;) {\n\t\t\t\tconst { value, done } = await reader.read()\n\t\t\t\tif (done) break\n\t\t\t\t// Pair the streaming decoder with the line parser: the decoder handles\n\t\t\t\t// partial multi-byte CHARS, the parser handles partial LINES (§14).\n\t\t\t\tfor (const record of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t\tyield* this.#deltas(record, state)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Flush the decoder's held partial multi-byte tail and feed it (plus a\n\t\t\t// terminating `\\n`) through the parser, so a non-conformant proxy's final\n\t\t\t// unterminated `done` line is recovered instead of silently dropped.\n\t\t\tconst decoderTail = decoder.decode()\n\t\t\tfor (const record of parser.parse(decoderTail.length > 0 ? `${decoderTail}\\n` : '\\n')) {\n\t\t\t\tyield* this.#deltas(record, state)\n\t\t\t}\n\t\t\t// Stream end: a held partial tag that never completed was real content — it is the\n\t\t\t// final delta (the splitter folds it into its `content` too).\n\t\t\tconst tail = splitter.flush()\n\t\t\tif (tail.length > 0) yield { type: 'content', text: tail }\n\t\t} catch (error) {\n\t\t\t// A mid-stream cancel (the caller's signal or the deadline) surfaces the\n\t\t\t// partial so the loop can recover what streamed; anything else propagates.\n\t\t\tif (combined.aborted) {\n\t\t\t\t// Flush the splitter's held partial tail first (mirrors the\n\t\t\t\t// normal-completion assembly above) so the recovered partial includes\n\t\t\t\t// any clean content that never crossed a tag boundary.\n\t\t\t\tsplitter.flush()\n\t\t\t\tthrow new ProviderAbortError(\n\t\t\t\t\tthis.#result(\n\t\t\t\t\t\tsplitter.content,\n\t\t\t\t\t\tthis.#thought(splitter, state.wired),\n\t\t\t\t\t\tstate.calls,\n\t\t\t\t\t\tstate.usage,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\t\t\tthrow error\n\t\t} finally {\n\t\t\t// Cancel (not merely release) the reader on early return so the\n\t\t\t// underlying HTTP connection is freed; a normal-done or already-errored\n\t\t\t// reader tolerates the redundant cancel as a no-op. `cancel()` also\n\t\t\t// releases the lock — never call `releaseLock()` afterward.\n\t\t\ttry {\n\t\t\t\tawait reader.cancel()\n\t\t\t} catch {\n\t\t\t\t// Never mask the primary error/result with a cancel failure.\n\t\t\t}\n\t\t\tparser.reset()\n\t\t\ttimeout.clear()\n\t\t}\n\t\treturn this.#result(\n\t\t\tsplitter.content,\n\t\t\tthis.#thought(splitter, state.wired),\n\t\t\tstate.calls,\n\t\t\tstate.usage,\n\t\t)\n\t}\n\n\t// Per-record streaming step shared between the live NDJSON loop and the post-loop\n\t// tail flush in `stream()` — a `#` private method (not a free helper) because it\n\t// calls sibling methods (`this.#content` / `#thinking` / `#tools` / `#usage`) and\n\t// mutates the caller's `state` accumulator (`wired` / `calls` / `usage`) across\n\t// repeated calls sharing the same object reference.\n\t*#deltas(\n\t\trecord: Record<string, unknown>,\n\t\tstate: {\n\t\t\tsplitter: ThinkSplitterInterface\n\t\t\twired: string\n\t\t\tcalls: ToolCall[]\n\t\t\tusage: TokenUsage | undefined\n\t\t},\n\t): Generator<ProviderDelta> {\n\t\tconst delta = state.splitter.split(this.#content(record))\n\t\tif (delta.length > 0) yield { type: 'content', text: delta }\n\t\t// The PRIMARY live reasoning channel: each native `message.thinking` wire delta is\n\t\t// surfaced as a tagged `thinking` delta AND accumulated into `state.wired` for the\n\t\t// assembled result (the two stay in lockstep). The ThinkSplitter's in-content\n\t\t// reclassified spans have no per-delta hook — the final `ProviderResult.thinking`\n\t\t// reconciles them; the native channel (think: true) is what streams live.\n\t\tconst thinking = this.#thinking(record)\n\t\tif (thinking.length > 0) yield { type: 'thinking', text: thinking }\n\t\tstate.wired += thinking\n\t\tstate.calls.push(...this.#tools(record))\n\t\tif (Reflect.get(record, 'done') === true) state.usage = this.#usage(record)\n\t}\n\n\t// Arm the deadline, POST `/api/chat`, and hand back the response + the handles\n\t// that bound it. On a non-OK status, clear the deadline and throw with the body.\n\tasync #fetch(\n\t\tmessages: readonly MessageInterface[],\n\t\tstream: boolean,\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<OllamaResponse> {\n\t\tconst timeout = new Timeout({ ms: this.#timeout })\n\t\ttimeout.start()\n\t\tconst combined = AbortSignal.any([timeout.signal, signal])\n\t\ttry {\n\t\t\tconst response = await this.#transport(`${this.#url}/api/chat`, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: await this.#requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.#body(messages, stream, tools, options)),\n\t\t\t\tsignal: combined,\n\t\t\t})\n\t\t\tif (!response.ok) {\n\t\t\t\t// Bound the incorporated body: a defensive proxy or daemon could hand\n\t\t\t\t// back an unbounded response — read defensively so a body-read\n\t\t\t\t// failure still throws with the status, never a masked/unbounded read.\n\t\t\t\tlet detail: string\n\t\t\t\ttry {\n\t\t\t\t\tconst text = await response.text()\n\t\t\t\t\tdetail = text.length > MAX_ERROR_BODY_LENGTH ? text.slice(0, MAX_ERROR_BODY_LENGTH) : text\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t\t`Ollama API error: ${response.status} - (error body unavailable)`,\n\t\t\t\t\t\tresponse.status,\n\t\t\t\t\t\t{ cause },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t`Ollama API error: ${response.status} - ${detail}`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn { response, timeout, combined }\n\t\t} catch (error) {\n\t\t\t// `fetch` rejected (pre-aborted signal / unreachable / network) or the status\n\t\t\t// was non-OK — clear the deadline so the armed timer can't outlive the failed\n\t\t\t// call. The caller's `finally` only takes ownership once we return a response.\n\t\t\ttimeout.clear()\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t// The non-stream `/api/chat` body — read the 200-OK response as text and parse it\n\t// inside a total guard (§14): a malformed or empty body degrades to `{}` (empty\n\t// content, no usage), never a raw `SyntaxError` escaping to the caller.\n\tasync #parseBody(response: Response): Promise<Record<string, unknown>> {\n\t\tconst text = await response.text()\n\t\tif (text.length === 0) return {}\n\t\ttry {\n\t\t\tconst data: unknown = JSON.parse(text)\n\t\t\treturn isRecord(data) ? data : {}\n\t\t} catch {\n\t\t\treturn {}\n\t\t}\n\t}\n\n\t// The request headers — the base JSON content type, plus the dynamic `headers`\n\t// hook's result merged ON TOP when configured (so a dev can attach an obfuscated\n\t// bearer the server validates). Merge order: `Content-Type` is seeded first, then\n\t// the hook's entries overlay it — so the hook ADDS auth headers but only clobbers\n\t// `Content-Type` if the dev explicitly returns one. Awaited (the hook may be async,\n\t// e.g. refreshing a token); called inside `#fetch`'s try so a hook rejection clears\n\t// the armed deadline like any other request failure. §14: the hook's result is a\n\t// `Record<string, string>` already — merged via `Object.entries`, no `as`.\n\tasync #requestHeaders(): Promise<Record<string, string>> {\n\t\tconst headers: Record<string, string> = { 'Content-Type': 'application/json' }\n\t\tif (this.#headers !== undefined) {\n\t\t\tfor (const [key, value] of Object.entries(await this.#headers())) headers[key] = value\n\t\t}\n\t\treturn headers\n\t}\n\n\t// The `/api/chat` request body — conditional `options` / `tools` / `format` only when set. The\n\t// wire `think` flag honours a PER-CALL override (`options.think`) over the constructor default\n\t// (`#think`), so a caller can flip reasoning on / off for one turn without reconfiguring the\n\t// provider; no per-call option ⇒ the constructed default, byte-for-byte the prior behaviour.\n\t// `format` is the wire's structured-output constraint, forwarded verbatim from the per-call\n\t// `ProviderStreamOptions.schema` — unrelated to `OllamaOptions.format` (prompt-context framing).\n\t#body(\n\t\tmessages: readonly MessageInterface[],\n\t\tstream: boolean,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): WireChatRequest {\n\t\treturn {\n\t\t\tmodel: this.#model,\n\t\t\tmessages: this.#plain(messages),\n\t\t\tstream,\n\t\t\tkeep_alive: this.#keepAlive,\n\t\t\tthink: options?.think ?? this.#think,\n\t\t\t...(this.#options !== undefined ? { options: this.#options } : {}),\n\t\t\t...(options?.schema !== undefined ? { format: options.schema } : {}),\n\t\t\t...(tools !== undefined && tools.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\ttools: tools.map(\n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\ttool,\n\t\t\t\t\t\t\t): {\n\t\t\t\t\t\t\t\ttype: 'function'\n\t\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\t\tname: string\n\t\t\t\t\t\t\t\t\tdescription?: string\n\t\t\t\t\t\t\t\t\tparameters?: Readonly<Record<string, unknown>>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} => ({\n\t\t\t\t\t\t\t\ttype: 'function',\n\t\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\t\tname: tool.name,\n\t\t\t\t\t\t\t\t\t...(tool.description === undefined ? {} : { description: tool.description }),\n\t\t\t\t\t\t\t\t\t...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t}\n\t}\n\n\t// Map messages to the wire's minimal turn shape — `tool_calls` only on a turn\n\t// that replays them, `images` only on a multimodal turn (omit empty optionals).\n\t#plain(messages: readonly MessageInterface[]): WireChatRequest['messages'] {\n\t\treturn messages.map((message) => ({\n\t\t\trole: message.role,\n\t\t\tcontent: message.content,\n\t\t\t...(message.calls !== undefined && message.calls.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\ttool_calls: message.calls.map((call) => ({\n\t\t\t\t\t\t\tfunction: { name: call.name, arguments: call.arguments },\n\t\t\t\t\t\t})),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\t// Forward multimodal image data — Ollama accepts a base64 `images` array on a\n\t\t\t// message, which a vision-capable model receives alongside the text content.\n\t\t\t...(message.images !== undefined && message.images.length > 0\n\t\t\t\t? { images: [...message.images] }\n\t\t\t\t: {}),\n\t\t}))\n\t}\n\n\t// Assemble a ProviderResult including only the present optionals — no empty\n\t// `thinking` / `tools`, no `usage` unless the wire reported it.\n\t#result(\n\t\tcontent: string,\n\t\tthinking: string,\n\t\ttools: readonly ToolCall[],\n\t\tusage: TokenUsage | undefined,\n\t): ProviderResult {\n\t\tconst result: {\n\t\t\tcontent: string\n\t\t\tthinking?: string\n\t\t\ttools?: readonly ToolCall[]\n\t\t\tusage?: TokenUsage\n\t\t} = { content }\n\t\tif (thinking.length > 0) result.thinking = thinking\n\t\tif (tools.length > 0) result.tools = tools\n\t\tif (usage !== undefined) result.usage = usage\n\t\treturn result\n\t}\n\n\t// The assistant text of one wire record — `message.content` when a string, else\n\t// `''` (a delta line, a tool-only turn, or a malformed shape).\n\t#content(record: Record<string, unknown>): string {\n\t\tconst message = Reflect.get(record, 'message')\n\t\tif (!isRecord(message)) return ''\n\t\tconst content = Reflect.get(message, 'content')\n\t\treturn isString(content) ? content : ''\n\t}\n\n\t// The daemon-side reasoning of one wire record — `message.thinking` when a string\n\t// (the `think: true` wire shape — surfaced when the configured `think` flag is on, and\n\t// handled defensively regardless since the daemon may vary), else `''`.\n\t#thinking(record: Record<string, unknown>): string {\n\t\tconst message = Reflect.get(record, 'message')\n\t\tif (!isRecord(message)) return ''\n\t\tconst thinking = Reflect.get(message, 'thinking')\n\t\treturn isString(thinking) ? thinking : ''\n\t}\n\n\t// Join a call's two reasoning carriers — the splitter's separated in-content spans and\n\t// the accumulated wire-side `message.thinking` — blank-line separated when both exist.\n\t#thought(splitter: ThinkSplitterInterface, wired: string): string {\n\t\tif (splitter.thinking.length === 0) return wired\n\t\tif (wired.length === 0) return splitter.thinking\n\t\treturn `${splitter.thinking}\\n\\n${wired}`\n\t}\n\n\t// Token usage from a wire record — only when BOTH counts are numbers (`done`\n\t// line / non-stream body); a delta line carries neither, so it yields undefined.\n\t#usage(record: Record<string, unknown>): TokenUsage | undefined {\n\t\tconst prompt = Reflect.get(record, 'prompt_eval_count')\n\t\tconst completion = Reflect.get(record, 'eval_count')\n\t\tif (!isNumber(prompt) || !isNumber(completion)) return undefined\n\t\treturn { prompt, completion, total: prompt + completion }\n\t}\n\n\t// Tool calls from a wire record's `message.tool_calls` — each entry narrowed to\n\t// `{ id, name, arguments }`, minting an id when the wire omits one and coercing a\n\t// JSON-string `arguments` to a record (defaulting to `{}`); §14, no `as`.\n\t#tools(record: Record<string, unknown>): readonly ToolCall[] {\n\t\tconst message = Reflect.get(record, 'message')\n\t\tif (!isRecord(message)) return []\n\t\tconst calls = Reflect.get(message, 'tool_calls')\n\t\tif (!Array.isArray(calls)) return []\n\t\tconst out: ToolCall[] = []\n\t\tfor (const entry of calls) {\n\t\t\tif (!isRecord(entry)) continue\n\t\t\tconst callable = Reflect.get(entry, 'function')\n\t\t\tif (!isRecord(callable)) continue\n\t\t\tconst name = Reflect.get(callable, 'name')\n\t\t\tif (!isString(name)) continue\n\t\t\tconst id = Reflect.get(entry, 'id')\n\t\t\tout.push({\n\t\t\t\tid: isString(id) ? id : crypto.randomUUID(),\n\t\t\t\tname,\n\t\t\t\targuments: this.#arguments(Reflect.get(callable, 'arguments')),\n\t\t\t})\n\t\t}\n\t\treturn out\n\t}\n\n\t// Narrow a wire `arguments` value to a record — an object as-is, a JSON string\n\t// parsed (when it yields a record), otherwise `{}`. Total: a bad string never throws.\n\t#arguments(value: unknown): Readonly<Record<string, unknown>> {\n\t\tif (isRecord(value)) return value\n\t\tif (isString(value)) {\n\t\t\ttry {\n\t\t\t\tconst parsed: unknown = JSON.parse(value)\n\t\t\t\tif (isRecord(parsed)) return parsed\n\t\t\t} catch {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t}\n\t\treturn {}\n\t}\n}\n","import type { ProviderInterface } from '@orkestrel/agent'\nimport type { OllamaOptions } from './types.js'\nimport { OllamaProvider } from './OllamaProvider.js'\n\n/**\n * Create a local Ollama inference provider — a {@link ProviderInterface} over the\n * daemon's `POST /api/chat`, supporting non-streaming `generate` and streaming\n * `stream`.\n *\n * @remarks\n * Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,\n * `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling\n * parameters (`temperature` / `seed` / `num_predict` / …). Both calls take an\n * `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result.\n *\n * The optional `fetch` + `headers` form a transport seam (see {@link OllamaOptions}):\n * point `url` at your own server, inject a custom `fetch`, and have `headers` attach a\n * generated/obfuscated bearer token your server validates — so a browser runtime\n * reaches the LLM through your middleware WITHOUT this library ever handling the real API\n * key. Both omitted ⇒ today's behaviour (the global `fetch`, only a JSON content type).\n *\n * The optional `format` is the provider's context-framing default — the PROVIDER-DEFAULT\n * level of `AgentContext`'s format cascade (see [agents.md]; beaten by a manager-options\n * or per-item override, beating the managers' built-in framing), declaring how this\n * provider's models prefer context sections framed (e.g. XML group wrappers vs. Markdown\n * headers). It is EXPOSED on the provider for the Agent's `build()` and is NOT Ollama's\n * `/api/chat` `format` wire parameter (structured output) — the two are unrelated despite\n * the shared word. Omitted ⇒ the provider is framing-agnostic (core's built-in defaults).\n *\n * @param options - `model` (required), and optional `url` / `keepAlive` / `timeout` /\n * `options` / `fetch` / `headers` / `format` (see {@link OllamaOptions})\n * @returns A working {@link ProviderInterface} backed by Ollama\n *\n * @example\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import { createOllama } from '@src/server'\n *\n * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M' })\n * const abort = createAbort()\n * const result = await provider.generate(messages, abort.signal)\n * ```\n *\n * @example\n * Route through your own server with an obfuscated token (deployment scenario S2):\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * url: 'https://my-app.example.com/llm', // your server, not the daemon\n * fetch: myFetch, // optional custom transport\n * headers: () => ({ authorization: `Bearer ${myToken}` }), // your server validates this\n * })\n * ```\n *\n * @example\n * Declare a context-framing default — wrap the instructions section in an XML group (the\n * provider-default level of `AgentContext`'s cascade; NOT the wire `format`):\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * format: {\n * instructions: {\n * open: '<instructions>',\n * render: (i) => `<instruction>${i.content}</instruction>`,\n * close: '</instructions>',\n * },\n * },\n * })\n * ```\n */\nexport function createOllama(options: OllamaOptions): ProviderInterface {\n\treturn new OllamaProvider(options)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#model","#url","#keepAlive","#timeout","#think","#options","#transport","#headers","#format","#fetch","#parseBody","#content","#thought","#thinking","#result","#tools","#usage","#deltas","#requestHeaders","#body","#plain","#arguments"],"sources":["../../../src/server/constants.ts","../../../src/server/errors.ts","../../../src/server/OllamaProvider.ts","../../../src/server/factories.ts"],"sourcesContent":["// Ollama constants — the provider's defaults (AGENTS §5).\n\n/** The local Ollama daemon base URL assumed when `OllamaOptions.url` is omitted. */\nexport const DEFAULT_OLLAMA_URL = 'http://localhost:11434'\n\n/**\n * How long the model stays resident after a call when `OllamaOptions.keepAlive` is\n * omitted — Ollama's own `keep_alive` default, expressed as a duration string.\n */\nexport const DEFAULT_KEEP_ALIVE = '5m'\n\n/**\n * The per-call deadline in milliseconds when `OllamaOptions.timeout` is omitted —\n * generous enough that a cold model load does not trip it.\n */\nexport const DEFAULT_PROVIDER_TIMEOUT = 120_000\n\n/**\n * The cap, in characters, on how much of a non-OK response body is\n * incorporated into a thrown {@link OllamaHTTPError}'s message.\n *\n * @remarks\n * Bounds the excerpt so a defensive proxy or a misbehaving daemon handing\n * back an unbounded response body cannot inflate the thrown error's message\n * without limit (§14). `2048` characters is generous enough to carry a\n * useful diagnostic snippet while staying well short of any practical size\n * concern.\n */\nexport const MAX_ERROR_BODY_LENGTH = 2048\n","// Errors for the Ollama provider. A single `OllamaHTTPError` carries the\n// `/api/chat` HTTP status at the boundary — non-OK responses and a missing\n// response body both throw it — so a `catch` can branch on `error.status`\n// rather than parsing a message (AGENTS §12).\n\n/**\n * An error thrown when the Ollama `/api/chat` HTTP transport fails.\n *\n * @remarks\n * Carries the response `status` (0 when no HTTP response was received at all,\n * e.g. a `null` body). Thrown by {@link OllamaProvider} at its two HTTP\n * failure sites — the non-OK status branch and the null-body branch — so a\n * caller can branch on `error.status` instead of parsing the message. Narrow\n * a caught value with {@link isOllamaHTTPError}.\n *\n * @example\n * ```ts\n * try {\n * \tawait provider.generate(messages, signal)\n * } catch (error) {\n * \tif (isOllamaHTTPError(error) && error.status === 404) {\n * \t\t// the configured model isn't pulled\n * \t}\n * }\n * ```\n */\nexport class OllamaHTTPError extends Error {\n\treadonly status: number\n\n\tconstructor(message: string, status: number, options?: { readonly cause?: unknown }) {\n\t\tsuper(message, options)\n\t\tthis.name = 'OllamaHTTPError'\n\t\tthis.status = status\n\t}\n}\n\n/**\n * Whether a value is an {@link OllamaHTTPError}.\n *\n * @param value - The value to test\n * @returns `true` when `value` is an `OllamaHTTPError`\n */\nexport function isOllamaHTTPError(value: unknown): value is OllamaHTTPError {\n\treturn value instanceof OllamaHTTPError\n}\n","import type {\n\tContextFormatInterface,\n\tMessageInterface,\n\tProviderDelta,\n\tProviderInterface,\n\tProviderResult,\n\tProviderStreamOptions,\n\tThinkSplitterInterface,\n} from '@orkestrel/agent'\nimport type { TokenUsage } from '@orkestrel/budget'\nimport type { ToolCall, ToolDefinition } from '@orkestrel/tool'\nimport type { OllamaOptions, OllamaResponse, WireChatRequest } from './types.js'\nimport { createThinkSplitter, ProviderAbortError } from '@orkestrel/agent'\nimport { isNumber, isRecord, isString } from '@orkestrel/contract'\nimport { createNDJSONParser } from '@orkestrel/ndjson'\nimport { Timeout } from '@orkestrel/timeout'\nimport {\n\tDEFAULT_KEEP_ALIVE,\n\tDEFAULT_OLLAMA_URL,\n\tDEFAULT_PROVIDER_TIMEOUT,\n\tMAX_ERROR_BODY_LENGTH,\n} from './constants.js'\nimport { OllamaHTTPError } from './errors.js'\n\n/**\n * The local Ollama inference boundary — a {@link ProviderInterface} over Ollama's\n * `POST /api/chat`, both non-streaming (`generate`) and streaming NDJSON (`stream`).\n *\n * @remarks\n * - **Wire protocol.** Posts `{ model, messages, stream, keep_alive, think }` plus\n * passthrough sampling `options` and mapped function `tools`. The `think` flag is\n * CONFIGURABLE via {@link OllamaOptions.think} (default `false`). Non-stream parses\n * one JSON body; stream consumes NDJSON (one JSON object per `\\n`-terminated line) —\n * deltas carry `message.content`, the final `done: true` line carries the token usage.\n * - **Think separation (H4).** The wire `think` flag is configurable\n * ({@link OllamaOptions.think}, default `false`). With `think: true` a thinking model's\n * daemon separates reasoning NATIVELY — returning it on the distinct `message.thinking`\n * channel (read here via `#thinking`) instead of inline in `message.content`. EITHER\n * way the per-call {@link ThinkSplitterInterface} is the defensive guarantee: a daemon\n * may ignore `think: false` for a thinking model and inline `<think>` tags, so every\n * content delta routes through the splitter, only CLEAN content is yielded / assembled,\n * and the separated reasoning (plus any daemon-side `message.thinking` deltas) lands on\n * `ProviderResult.thinking`, never in the conversation.\n * - **Boundary narrowing (§14).** Every wire value arrives as `unknown` and is\n * narrowed through guards (`isRecord` / `isString` / `isNumber`) — never `as`. A\n * missing / malformed field degrades to a sensible default (empty content, no\n * usage, `{}` arguments), never a throw.\n * - **Bounded.** Each call arms a {@link Timeout} for `OllamaOptions.timeout` and\n * passes `AbortSignal.any([timeout.signal, signal])` to `fetch`, so the caller's\n * signal AND the deadline both cancel the request. The timeout is always cleared —\n * in `#fetch` if the request fails/aborts, otherwise in the consuming call's `finally`.\n * - **Abort recovers partial.** A `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result assembled so far; pairing the\n * `TextDecoder({ stream: true })` with the {@link NDJSONParser} parser keeps multi-byte\n * UTF-8 splits and partial lines honest.\n * - **Event-free.** A pure functional boundary — no Emitter, no events.\n * - **Transport seam.** {@link OllamaOptions.fetch} swaps the transport (default\n * `globalThis.fetch`) and {@link OllamaOptions.headers} is a per-request, possibly\n * async header injector merged over the base `Content-Type` — so a browser runtime\n * can route through the developer's own server with an obfuscated bearer token,\n * without this library ever handling a real API key. Both omitted ⇒ today's behaviour.\n * Orthogonal to the deadline: the hook is awaited inside `#fetch`'s try, so a hook\n * rejection clears the armed timer like any other request failure.\n *\n * @example\n * ```ts\n * const provider = new OllamaProvider({ model: 'qwen3.5:2b-q4_K_M' })\n * const result = await provider.generate(messages, abort.signal)\n * ```\n */\nexport class OllamaProvider implements ProviderInterface {\n\treadonly id = crypto.randomUUID()\n\treadonly name = 'ollama'\n\treadonly #model: string\n\treadonly #url: string\n\treadonly #keepAlive: string | number\n\treadonly #timeout: number\n\treadonly #think: boolean\n\treadonly #options: Readonly<Record<string, unknown>> | undefined\n\treadonly #transport: typeof globalThis.fetch\n\treadonly #headers: (() => Record<string, string> | Promise<Record<string, string>>) | undefined\n\treadonly #format: ContextFormatInterface | undefined\n\n\tconstructor(options: OllamaOptions) {\n\t\tthis.#model = options.model\n\t\tthis.#url = options.url ?? DEFAULT_OLLAMA_URL\n\t\tthis.#keepAlive = options.keepAlive ?? DEFAULT_KEEP_ALIVE\n\t\tthis.#timeout = options.timeout ?? DEFAULT_PROVIDER_TIMEOUT\n\t\t// The `/api/chat` `think` wire flag — DEFAULT `false` so the general-purpose provider\n\t\t// stays backward-compatible and immediate for non-thinking models. A thinking model whose\n\t\t// reasoning is DISPLAYED separately sets `think: true`, and the daemon then returns it on\n\t\t// the `message.thinking` channel (`#thinking`) rather than inline in `message.content`.\n\t\tthis.#think = options.think ?? false\n\t\tthis.#options = options.options\n\t\t// The transport seam (§21): a custom fetch (defaulting to the global, BOUND to its\n\t\t// `globalThis` receiver — invoking a bare reference through a field loses the `window`\n\t\t// receiver and browsers throw `Illegal invocation`; node's fetch is receiver-agnostic,\n\t\t// so only a browser runtime ever saw it) and a dynamic header injector — both omitted\n\t\t// by default, so today's behaviour is byte-identical (the global fetch, only the JSON\n\t\t// content type). The injected transport is `#transport` (the request METHOD already\n\t\t// owns the `#fetch` name).\n\t\tthis.#transport = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#headers = options.headers\n\t\t// The context-framing default (the provider-DEFAULT level of AgentContext's format\n\t\t// cascade) — EXPOSE-ONLY: read by the Agent via `build(this.#provider.format)` and\n\t\t// consumed by core's cascade, it NEVER enters `#body` / the `/api/chat` wire. It is\n\t\t// NOT Ollama's structured-output `format` wire param — that one IS sent in `#body`,\n\t\t// but only when a per-call `ProviderStreamOptions.schema` is supplied; the two\n\t\t// merely share a word. Omitted ⇒ undefined ⇒ core's built-in framing.\n\t\tthis.#format = options.format\n\t}\n\n\t/**\n\t * The provider's context-framing default — the PROVIDER-DEFAULT level of\n\t * {@link import('@orkestrel/agent').AgentContextInterface.build}'s format cascade (it BEATS\n\t * the managers' built-in framing, is BEATEN by a manager-options or per-item override).\n\t * Satisfies the OPTIONAL {@link ProviderInterface.format} contract member: `undefined`\n\t * when {@link OllamaOptions.format} was omitted (the framing-agnostic default ⇒ core's\n\t * built-in framing applies unchanged), else the exact configured framing the Agent\n\t * threads into `build()`.\n\t *\n\t * @remarks\n\t * EXPOSE-ONLY — read by the Agent loop and consumed by core's cascade; it is NEVER sent\n\t * on the `/api/chat` wire (it is absent from `#body` / the request). This is NOT Ollama's\n\t * structured-output `format` wire parameter — that one IS sent in `#body`, but only when\n\t * a per-call `ProviderStreamOptions.schema` is supplied; only the word collides.\n\t *\n\t * @returns The configured {@link ContextFormatInterface}, or `undefined` when none\n\t */\n\tget format(): ContextFormatInterface | undefined {\n\t\treturn this.#format\n\t}\n\n\tasync generate(\n\t\tmessages: readonly MessageInterface[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<ProviderResult> {\n\t\tconst { response, timeout } = await this.#fetch(messages, false, signal, tools, options)\n\t\ttry {\n\t\t\tconst record = await this.#parseBody(response)\n\t\t\t// The one-body call routes through the SAME splitter as the stream (the daemon may\n\t\t\t// ignore `think: false` — the splitter is the guarantee): the assembled content is\n\t\t\t// CLEAN (the splitter's authoritative `content`, which also covers the qwen3\n\t\t\t// template's IMPLICIT leading open), the separated spans + any wire-side\n\t\t\t// `message.thinking` land on `thinking`.\n\t\t\tconst splitter = createThinkSplitter()\n\t\t\tsplitter.split(this.#content(record))\n\t\t\tsplitter.flush()\n\t\t\tconst thinking = this.#thought(splitter, this.#thinking(record))\n\t\t\treturn this.#result(splitter.content, thinking, this.#tools(record), this.#usage(record))\n\t\t} finally {\n\t\t\ttimeout.clear()\n\t\t}\n\t}\n\n\tasync *stream(\n\t\tmessages: readonly MessageInterface[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): AsyncGenerator<ProviderDelta, ProviderResult> {\n\t\tconst { response, timeout, combined } = await this.#fetch(\n\t\t\tmessages,\n\t\t\ttrue,\n\t\t\tsignal,\n\t\t\ttools,\n\t\t\toptions,\n\t\t)\n\t\tconst body = response.body\n\t\tif (body === null) {\n\t\t\ttimeout.clear()\n\t\t\tthrow new OllamaHTTPError('Ollama API error: no response body', 0)\n\t\t}\n\t\tconst reader = body.getReader()\n\t\tconst decoder = new TextDecoder()\n\t\tconst parser = createNDJSONParser()\n\t\t// The per-call think separator (H4): every wire content delta routes through it, so\n\t\t// only CLEAN content is yielded / assembled even when the daemon ignores `think: false`\n\t\t// for a thinking model; daemon-side `message.thinking` deltas accumulate beside it.\n\t\t// The ASSEMBLED content is the splitter's authoritative `content` — across the qwen3\n\t\t// template's IMPLICIT leading open (a bare `</think>` with the open pre-seeded into the\n\t\t// prompt scaffold) the splitter RECLASSIFIES the already-yielded prefix into `thinking`,\n\t\t// so the result stays clean even though those deltas could not be recalled.\n\t\tconst splitter = createThinkSplitter()\n\t\t// Mutable per-stream accumulator shared between the live loop and the post-loop\n\t\t// NDJSON tail flush below — the SAME object reference threads through every\n\t\t// `#deltas` call so `wired` / `calls` / `usage` accumulate across both sites.\n\t\tconst state: {\n\t\t\tsplitter: ThinkSplitterInterface\n\t\t\twired: string\n\t\t\tcalls: ToolCall[]\n\t\t\tusage: TokenUsage | undefined\n\t\t} = {\n\t\t\tsplitter,\n\t\t\twired: '',\n\t\t\tcalls: [],\n\t\t\tusage: undefined,\n\t\t}\n\t\ttry {\n\t\t\tfor (;;) {\n\t\t\t\tconst { value, done } = await reader.read()\n\t\t\t\tif (done) break\n\t\t\t\t// Pair the streaming decoder with the line parser: the decoder handles\n\t\t\t\t// partial multi-byte CHARS, the parser handles partial LINES (§14).\n\t\t\t\tfor (const record of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t\tyield* this.#deltas(record, state)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Flush the decoder's held partial multi-byte tail and feed it (plus a\n\t\t\t// terminating `\\n`) through the parser, so a non-conformant proxy's final\n\t\t\t// unterminated `done` line is recovered instead of silently dropped.\n\t\t\tconst decoderTail = decoder.decode()\n\t\t\tfor (const record of parser.parse(decoderTail.length > 0 ? `${decoderTail}\\n` : '\\n')) {\n\t\t\t\tyield* this.#deltas(record, state)\n\t\t\t}\n\t\t\t// Stream end: a held partial tag that never completed was real content — it is the\n\t\t\t// final delta (the splitter folds it into its `content` too).\n\t\t\tconst tail = splitter.flush()\n\t\t\tif (tail.length > 0) yield { type: 'content', text: tail }\n\t\t} catch (error) {\n\t\t\t// A mid-stream cancel (the caller's signal or the deadline) surfaces the\n\t\t\t// partial so the loop can recover what streamed; anything else propagates.\n\t\t\tif (combined.aborted) {\n\t\t\t\t// Flush the splitter's held partial tail first (mirrors the\n\t\t\t\t// normal-completion assembly above) so the recovered partial includes\n\t\t\t\t// any clean content that never crossed a tag boundary.\n\t\t\t\tsplitter.flush()\n\t\t\t\tthrow new ProviderAbortError(\n\t\t\t\t\tthis.#result(\n\t\t\t\t\t\tsplitter.content,\n\t\t\t\t\t\tthis.#thought(splitter, state.wired),\n\t\t\t\t\t\tstate.calls,\n\t\t\t\t\t\tstate.usage,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\t\t\tthrow error\n\t\t} finally {\n\t\t\t// Cancel (not merely release) the reader on early return so the\n\t\t\t// underlying HTTP connection is freed; a normal-done or already-errored\n\t\t\t// reader tolerates the redundant cancel as a no-op. `cancel()` also\n\t\t\t// releases the lock — never call `releaseLock()` afterward.\n\t\t\ttry {\n\t\t\t\tawait reader.cancel()\n\t\t\t} catch {\n\t\t\t\t// Never mask the primary error/result with a cancel failure.\n\t\t\t}\n\t\t\tparser.reset()\n\t\t\ttimeout.clear()\n\t\t}\n\t\treturn this.#result(\n\t\t\tsplitter.content,\n\t\t\tthis.#thought(splitter, state.wired),\n\t\t\tstate.calls,\n\t\t\tstate.usage,\n\t\t)\n\t}\n\n\t// Per-record streaming step shared between the live NDJSON loop and the post-loop\n\t// tail flush in `stream()` — a `#` private method (not a free helper) because it\n\t// calls sibling methods (`this.#content` / `#thinking` / `#tools` / `#usage`) and\n\t// mutates the caller's `state` accumulator (`wired` / `calls` / `usage`) across\n\t// repeated calls sharing the same object reference.\n\t*#deltas(\n\t\trecord: Record<string, unknown>,\n\t\tstate: {\n\t\t\tsplitter: ThinkSplitterInterface\n\t\t\twired: string\n\t\t\tcalls: ToolCall[]\n\t\t\tusage: TokenUsage | undefined\n\t\t},\n\t): Generator<ProviderDelta> {\n\t\tconst delta = state.splitter.split(this.#content(record))\n\t\tif (delta.length > 0) yield { type: 'content', text: delta }\n\t\t// The PRIMARY live reasoning channel: each native `message.thinking` wire delta is\n\t\t// surfaced as a tagged `thinking` delta AND accumulated into `state.wired` for the\n\t\t// assembled result (the two stay in lockstep). The ThinkSplitter's in-content\n\t\t// reclassified spans have no per-delta hook — the final `ProviderResult.thinking`\n\t\t// reconciles them; the native channel (think: true) is what streams live.\n\t\tconst thinking = this.#thinking(record)\n\t\tif (thinking.length > 0) yield { type: 'thinking', text: thinking }\n\t\tstate.wired += thinking\n\t\tstate.calls.push(...this.#tools(record))\n\t\tif (Reflect.get(record, 'done') === true) state.usage = this.#usage(record)\n\t}\n\n\t// Arm the deadline, POST `/api/chat`, and hand back the response + the handles\n\t// that bound it. On a non-OK status, clear the deadline and throw with the body.\n\tasync #fetch(\n\t\tmessages: readonly MessageInterface[],\n\t\tstream: boolean,\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<OllamaResponse> {\n\t\tconst timeout = new Timeout({ ms: this.#timeout })\n\t\ttimeout.start()\n\t\tconst combined = AbortSignal.any([timeout.signal, signal])\n\t\ttry {\n\t\t\tconst response = await this.#transport(`${this.#url}/api/chat`, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: await this.#requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.#body(messages, stream, tools, options)),\n\t\t\t\tsignal: combined,\n\t\t\t})\n\t\t\tif (!response.ok) {\n\t\t\t\t// Bound the incorporated body: a defensive proxy or daemon could hand\n\t\t\t\t// back an unbounded response — read defensively so a body-read\n\t\t\t\t// failure still throws with the status, never a masked/unbounded read.\n\t\t\t\tlet detail: string\n\t\t\t\ttry {\n\t\t\t\t\tconst text = await response.text()\n\t\t\t\t\tdetail = text.length > MAX_ERROR_BODY_LENGTH ? text.slice(0, MAX_ERROR_BODY_LENGTH) : text\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t\t`Ollama API error: ${response.status} - (error body unavailable)`,\n\t\t\t\t\t\tresponse.status,\n\t\t\t\t\t\t{ cause },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t`Ollama API error: ${response.status} - ${detail}`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn { response, timeout, combined }\n\t\t} catch (error) {\n\t\t\t// `fetch` rejected (pre-aborted signal / unreachable / network) or the status\n\t\t\t// was non-OK — clear the deadline so the armed timer can't outlive the failed\n\t\t\t// call. The caller's `finally` only takes ownership once we return a response.\n\t\t\ttimeout.clear()\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t// The non-stream `/api/chat` body — read the 200-OK response as text and parse it\n\t// inside a total guard (§14): a malformed or empty body degrades to `{}` (empty\n\t// content, no usage), never a raw `SyntaxError` escaping to the caller.\n\tasync #parseBody(response: Response): Promise<Record<string, unknown>> {\n\t\tconst text = await response.text()\n\t\tif (text.length === 0) return {}\n\t\ttry {\n\t\t\tconst data: unknown = JSON.parse(text)\n\t\t\treturn isRecord(data) ? data : {}\n\t\t} catch {\n\t\t\treturn {}\n\t\t}\n\t}\n\n\t// The request headers — the base JSON content type, plus the dynamic `headers`\n\t// hook's result merged ON TOP when configured (so a dev can attach an obfuscated\n\t// bearer the server validates). Merge order: `Content-Type` is seeded first, then\n\t// the hook's entries overlay it — so the hook ADDS auth headers but only clobbers\n\t// `Content-Type` if the dev explicitly returns one. Awaited (the hook may be async,\n\t// e.g. refreshing a token); called inside `#fetch`'s try so a hook rejection clears\n\t// the armed deadline like any other request failure. §14: the hook's result is a\n\t// `Record<string, string>` already — merged via `Object.entries`, no `as`.\n\tasync #requestHeaders(): Promise<Record<string, string>> {\n\t\tconst headers: Record<string, string> = { 'Content-Type': 'application/json' }\n\t\tif (this.#headers !== undefined) {\n\t\t\tfor (const [key, value] of Object.entries(await this.#headers())) headers[key] = value\n\t\t}\n\t\treturn headers\n\t}\n\n\t// The `/api/chat` request body — conditional `options` / `tools` / `format` only when set. The\n\t// wire `think` flag honours a PER-CALL override (`options.think`) over the constructor default\n\t// (`#think`), so a caller can flip reasoning on / off for one turn without reconfiguring the\n\t// provider; no per-call option ⇒ the constructed default, byte-for-byte the prior behaviour.\n\t// `format` is the wire's structured-output constraint, forwarded verbatim from the per-call\n\t// `ProviderStreamOptions.schema` — unrelated to `OllamaOptions.format` (prompt-context framing).\n\t#body(\n\t\tmessages: readonly MessageInterface[],\n\t\tstream: boolean,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): WireChatRequest {\n\t\treturn {\n\t\t\tmodel: this.#model,\n\t\t\tmessages: this.#plain(messages),\n\t\t\tstream,\n\t\t\tkeep_alive: this.#keepAlive,\n\t\t\tthink: options?.think ?? this.#think,\n\t\t\t...(this.#options !== undefined ? { options: this.#options } : {}),\n\t\t\t...(options?.schema !== undefined ? { format: options.schema } : {}),\n\t\t\t...(tools !== undefined && tools.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\ttools: tools.map(\n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\ttool,\n\t\t\t\t\t\t\t): {\n\t\t\t\t\t\t\t\ttype: 'function'\n\t\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\t\tname: string\n\t\t\t\t\t\t\t\t\tdescription?: string\n\t\t\t\t\t\t\t\t\tparameters?: Readonly<Record<string, unknown>>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} => ({\n\t\t\t\t\t\t\t\ttype: 'function',\n\t\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\t\tname: tool.name,\n\t\t\t\t\t\t\t\t\t...(tool.description === undefined ? {} : { description: tool.description }),\n\t\t\t\t\t\t\t\t\t...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t}\n\t}\n\n\t// Map messages to the wire's minimal turn shape — `tool_calls` only on a turn\n\t// that replays them, `images` only on a multimodal turn (omit empty optionals).\n\t#plain(messages: readonly MessageInterface[]): WireChatRequest['messages'] {\n\t\treturn messages.map((message) => ({\n\t\t\trole: message.role,\n\t\t\tcontent: message.content,\n\t\t\t...(message.calls !== undefined && message.calls.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\ttool_calls: message.calls.map((call) => ({\n\t\t\t\t\t\t\tfunction: { name: call.name, arguments: call.arguments },\n\t\t\t\t\t\t})),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\t// Forward multimodal image data — Ollama accepts a base64 `images` array on a\n\t\t\t// message, which a vision-capable model receives alongside the text content.\n\t\t\t...(message.images !== undefined && message.images.length > 0\n\t\t\t\t? { images: [...message.images] }\n\t\t\t\t: {}),\n\t\t}))\n\t}\n\n\t// Assemble a ProviderResult including only the present optionals — no empty\n\t// `thinking` / `tools`, no `usage` unless the wire reported it.\n\t#result(\n\t\tcontent: string,\n\t\tthinking: string,\n\t\ttools: readonly ToolCall[],\n\t\tusage: TokenUsage | undefined,\n\t): ProviderResult {\n\t\tconst result: {\n\t\t\tcontent: string\n\t\t\tthinking?: string\n\t\t\ttools?: readonly ToolCall[]\n\t\t\tusage?: TokenUsage\n\t\t} = { content }\n\t\tif (thinking.length > 0) result.thinking = thinking\n\t\tif (tools.length > 0) result.tools = tools\n\t\tif (usage !== undefined) result.usage = usage\n\t\treturn result\n\t}\n\n\t// The assistant text of one wire record — `message.content` when a string, else\n\t// `''` (a delta line, a tool-only turn, or a malformed shape).\n\t#content(record: Record<string, unknown>): string {\n\t\tconst message = Reflect.get(record, 'message')\n\t\tif (!isRecord(message)) return ''\n\t\tconst content = Reflect.get(message, 'content')\n\t\treturn isString(content) ? content : ''\n\t}\n\n\t// The daemon-side reasoning of one wire record — `message.thinking` when a string\n\t// (the `think: true` wire shape — surfaced when the configured `think` flag is on, and\n\t// handled defensively regardless since the daemon may vary), else `''`.\n\t#thinking(record: Record<string, unknown>): string {\n\t\tconst message = Reflect.get(record, 'message')\n\t\tif (!isRecord(message)) return ''\n\t\tconst thinking = Reflect.get(message, 'thinking')\n\t\treturn isString(thinking) ? thinking : ''\n\t}\n\n\t// Join a call's two reasoning carriers — the splitter's separated in-content spans and\n\t// the accumulated wire-side `message.thinking` — blank-line separated when both exist.\n\t#thought(splitter: ThinkSplitterInterface, wired: string): string {\n\t\tif (splitter.thinking.length === 0) return wired\n\t\tif (wired.length === 0) return splitter.thinking\n\t\treturn `${splitter.thinking}\\n\\n${wired}`\n\t}\n\n\t// Token usage from a wire record — only when BOTH counts are numbers (`done`\n\t// line / non-stream body); a delta line carries neither, so it yields undefined.\n\t#usage(record: Record<string, unknown>): TokenUsage | undefined {\n\t\tconst prompt = Reflect.get(record, 'prompt_eval_count')\n\t\tconst completion = Reflect.get(record, 'eval_count')\n\t\tif (!isNumber(prompt) || !isNumber(completion)) return undefined\n\t\treturn { prompt, completion, total: prompt + completion }\n\t}\n\n\t// Tool calls from a wire record's `message.tool_calls` — each entry narrowed to\n\t// `{ id, name, arguments }`, minting an id when the wire omits one and coercing a\n\t// JSON-string `arguments` to a record (defaulting to `{}`); §14, no `as`.\n\t#tools(record: Record<string, unknown>): readonly ToolCall[] {\n\t\tconst message = Reflect.get(record, 'message')\n\t\tif (!isRecord(message)) return []\n\t\tconst calls = Reflect.get(message, 'tool_calls')\n\t\tif (!Array.isArray(calls)) return []\n\t\tconst out: ToolCall[] = []\n\t\tfor (const entry of calls) {\n\t\t\tif (!isRecord(entry)) continue\n\t\t\tconst callable = Reflect.get(entry, 'function')\n\t\t\tif (!isRecord(callable)) continue\n\t\t\tconst name = Reflect.get(callable, 'name')\n\t\t\tif (!isString(name)) continue\n\t\t\tconst id = Reflect.get(entry, 'id')\n\t\t\tout.push({\n\t\t\t\tid: isString(id) ? id : crypto.randomUUID(),\n\t\t\t\tname,\n\t\t\t\targuments: this.#arguments(Reflect.get(callable, 'arguments')),\n\t\t\t})\n\t\t}\n\t\treturn out\n\t}\n\n\t// Narrow a wire `arguments` value to a record — an object as-is, a JSON string\n\t// parsed (when it yields a record), otherwise `{}`. Total: a bad string never throws.\n\t#arguments(value: unknown): Readonly<Record<string, unknown>> {\n\t\tif (isRecord(value)) return value\n\t\tif (isString(value)) {\n\t\t\ttry {\n\t\t\t\tconst parsed: unknown = JSON.parse(value)\n\t\t\t\tif (isRecord(parsed)) return parsed\n\t\t\t} catch {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t}\n\t\treturn {}\n\t}\n}\n","import type { ProviderInterface } from '@orkestrel/agent'\nimport type { OllamaOptions } from './types.js'\nimport { OllamaProvider } from './OllamaProvider.js'\n\n/**\n * Create a local Ollama inference provider — a {@link ProviderInterface} over the\n * daemon's `POST /api/chat`, supporting non-streaming `generate` and streaming\n * `stream`.\n *\n * @remarks\n * Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,\n * `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling\n * parameters (`temperature` / `seed` / `num_predict` / …). Both calls take an\n * `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result.\n *\n * The optional `fetch` + `headers` form a transport seam (see {@link OllamaOptions}):\n * point `url` at your own server, inject a custom `fetch`, and have `headers` attach a\n * generated/obfuscated bearer token your server validates — so a browser runtime\n * reaches the LLM through your middleware WITHOUT this library ever handling the real API\n * key. Both omitted ⇒ today's behaviour (the global `fetch`, only a JSON content type).\n *\n * The optional `format` is the provider's context-framing default — the PROVIDER-DEFAULT\n * level of `AgentContext`'s format cascade (see [agents.md]; beaten by a manager-options\n * or per-item override, beating the managers' built-in framing), declaring how this\n * provider's models prefer context sections framed (e.g. XML group wrappers vs. Markdown\n * headers). It is EXPOSED on the provider for the Agent's `build()` and is NOT Ollama's\n * `/api/chat` `format` wire parameter (structured output) — the two are unrelated despite\n * the shared word. Omitted ⇒ the provider is framing-agnostic (core's built-in defaults).\n *\n * @param options - `model` (required), and optional `url` / `keepAlive` / `timeout` /\n * `options` / `fetch` / `headers` / `format` (see {@link OllamaOptions})\n * @returns A working {@link ProviderInterface} backed by Ollama\n *\n * @example\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import { createOllama } from '@src/server'\n *\n * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M' })\n * const abort = createAbort()\n * const result = await provider.generate(messages, abort.signal)\n * ```\n *\n * @example\n * Route through your own server with an obfuscated token (deployment scenario S2):\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * url: 'https://my-app.example.com/llm', // your server, not the daemon\n * fetch: myFetch, // optional custom transport\n * headers: () => ({ authorization: `Bearer ${myToken}` }), // your server validates this\n * })\n * ```\n *\n * @example\n * Declare a context-framing default — wrap the instructions section in an XML group (the\n * provider-default level of `AgentContext`'s cascade; NOT the wire `format`):\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * format: {\n * instructions: {\n * open: '<instructions>',\n * render: (i) => `<instruction>${i.content}</instruction>`,\n * close: '</instructions>',\n * },\n * },\n * })\n * ```\n */\nexport function createOllama(options: OllamaOptions): ProviderInterface {\n\treturn new OllamaProvider(options)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/ollama",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "A typed local-LLM provider for the @orkestrel line — a ProviderInterface over a local Ollama daemon's /api/chat with NDJSON streaming and guard-narrowed wire boundaries. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -65,34 +65,36 @@
|
|
|
65
65
|
"build": "npm run clean && npm run build:src",
|
|
66
66
|
"build:src": "npm run build:src:server",
|
|
67
67
|
"build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
|
|
68
|
-
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test && npm run test:service"
|
|
68
|
+
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test && npm run test:distribution -- --mode release && npm run test:service",
|
|
69
|
+
"test:distribution": "vitest run --config vite.config.ts --no-cache --reporter=dot --project distribution"
|
|
69
70
|
},
|
|
70
71
|
"dependencies": {
|
|
71
|
-
"@orkestrel/agent": "^0.0.
|
|
72
|
-
"@orkestrel/budget": "^0.0.
|
|
73
|
-
"@orkestrel/contract": "^0.0.
|
|
74
|
-
"@orkestrel/ndjson": "^0.0.
|
|
75
|
-
"@orkestrel/timeout": "^0.0.
|
|
76
|
-
"@orkestrel/tool": "^0.0.
|
|
72
|
+
"@orkestrel/agent": "^0.0.18",
|
|
73
|
+
"@orkestrel/budget": "^0.0.8",
|
|
74
|
+
"@orkestrel/contract": "^0.0.13",
|
|
75
|
+
"@orkestrel/ndjson": "^0.0.8",
|
|
76
|
+
"@orkestrel/timeout": "^0.0.8",
|
|
77
|
+
"@orkestrel/tool": "^0.0.12"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
|
-
"@microsoft/api-extractor": "^7.
|
|
80
|
-
"@orkestrel/abort": "^0.0.
|
|
81
|
-
"@orkestrel/guide": "^0.0.
|
|
82
|
-
"@orkestrel/
|
|
83
|
-
"@orkestrel/
|
|
84
|
-
"@orkestrel/
|
|
85
|
-
"@orkestrel/
|
|
86
|
-
"@orkestrel/
|
|
87
|
-
"@
|
|
88
|
-
"@
|
|
80
|
+
"@microsoft/api-extractor": "^7.59.0",
|
|
81
|
+
"@orkestrel/abort": "^0.0.8",
|
|
82
|
+
"@orkestrel/guide": "^0.0.13",
|
|
83
|
+
"@orkestrel/probe": "^0.0.3",
|
|
84
|
+
"@orkestrel/router": "^0.0.11",
|
|
85
|
+
"@orkestrel/scaffold": "^0.0.50",
|
|
86
|
+
"@orkestrel/server": "^0.0.15",
|
|
87
|
+
"@orkestrel/test": "^0.0.11",
|
|
88
|
+
"@orkestrel/workspace": "^0.0.6",
|
|
89
|
+
"@types/node": "^26.2.0",
|
|
90
|
+
"@vitest/browser-playwright": "^4.1.11",
|
|
89
91
|
"ollama": "0.6.3",
|
|
90
|
-
"oxfmt": "^0.
|
|
91
|
-
"oxlint": "^1.
|
|
92
|
+
"oxfmt": "^0.64.0",
|
|
93
|
+
"oxlint": "^1.79.0",
|
|
92
94
|
"typescript": "^6.0.3",
|
|
93
|
-
"vite": "^8.2.
|
|
95
|
+
"vite": "^8.2.2",
|
|
94
96
|
"vite-plugin-dts": "^5.0.3",
|
|
95
|
-
"vitest": "^4.1.
|
|
97
|
+
"vitest": "^4.1.11"
|
|
96
98
|
},
|
|
97
99
|
"engines": {
|
|
98
100
|
"node": ">=22.12.0"
|