@lihuu/dsh-ollama-cloud 0.2.0 → 0.2.1
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.
- package/README.md +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter.ts +1 -1
- package/src/index.ts +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin tha
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
- Registers the `ollama-cloud-direct` provider route on the LLM seam
|
|
8
|
-
- Declares the route in the configurable-provider directory as a **dormant entry**: until configured, it is offered by the Models page's **添加提供方** select (like a pi-ai route), and configuring it turns it into a removable **
|
|
8
|
+
- Declares the route in the configurable-provider directory as a **dormant entry**: until configured, it is offered by the Models page's **添加提供方** select (like a pi-ai route), and configuring it turns it into a removable **ollama-cloud** row
|
|
9
9
|
- Installs the `llm-ollama-cloud` user-settings section, so base URL, model catalog, and defaults are editable on the page and take effect without a restart
|
|
10
10
|
- Answers the Models page's **fetch available models** action from the resolved catalog, or interrogates a drafted endpoint at `GET {baseURL}/models`
|
|
11
11
|
- Ships with a default model catalog: DeepSeek-V4-Flash (cloud), DeepSeek-V4-Pro (cloud), GLM-5.2 (cloud)
|
|
@@ -52,7 +52,7 @@ Easiest path: drop the `config:` block entirely (keep the bare row) and configur
|
|
|
52
52
|
|
|
53
53
|
### 1. Set the API key
|
|
54
54
|
|
|
55
|
-
After a restart, open **Settings → Models**, click **添加提供方**, and pick **
|
|
55
|
+
After a restart, open **Settings → Models**, click **添加提供方**, and pick **ollama-cloud** from the select:
|
|
56
56
|
|
|
57
57
|
- Type the key and **保存** — it is stored write-only under the `OLLAMA_CLOUD_DIRECT_API_KEY` credential reference, and the profile is created. The row appears with a green key dot.
|
|
58
58
|
- Or save **without typing a key** — the profile resolves the conventional `OLLAMA_CLOUD_API_KEY` reference instead, so a key already set in the environment or the credentials file keeps working.
|
package/dist/index.js
CHANGED
|
@@ -576,7 +576,7 @@ var OllamaAdapter = class extends LlmAdapter {
|
|
|
576
576
|
}
|
|
577
577
|
config;
|
|
578
578
|
providerInfo(provider) {
|
|
579
|
-
return { id: provider, name: "
|
|
579
|
+
return { id: provider, name: "ollama-cloud" };
|
|
580
580
|
}
|
|
581
581
|
providerRetryPolicy(_provider) {
|
|
582
582
|
return this.config.options().retryPolicy;
|
|
@@ -1015,7 +1015,7 @@ function apply(ctx, config = {}) {
|
|
|
1015
1015
|
};
|
|
1016
1016
|
const adapter = new OllamaAdapter({ options, resolveApiKey });
|
|
1017
1017
|
ctx.llm.registerConfigurableProviders([
|
|
1018
|
-
{ provider: PROVIDER, displayName: "
|
|
1018
|
+
{ provider: PROVIDER, displayName: "ollama-cloud", settingsNs: NS, settingsPath: ["providers", PROVIDER] }
|
|
1019
1019
|
]);
|
|
1020
1020
|
const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
1021
1021
|
let registeredPolicy = options().retryPolicy;
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts", "../src/adapter.ts", "../src/serialize.ts", "../../../../git/deepseek-harness/node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/src/errors.ts", "../../../../git/deepseek-harness/node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/src/parse.ts", "../../../../git/deepseek-harness/node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/src/stream.ts", "../src/sse.ts", "../src/translate.ts", "../src/discovery.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Register an {@link OllamaAdapter} for the `ollama-cloud-direct` provider\n * route on `ctx.llm`, with connection facts resolved per request instead of\n * frozen at load: the plugin layers its `cordis.yml` entry config under the\n * optional `llm-ollama-cloud` user-settings section (`ctx.settings`) and\n * resolves the bearer token through the credential seam (`ctx.credentials`),\n * falling back to the process environment, so a changed base URL, catalog, or\n * key reaches the very next request without restarting anything, while an\n * in-flight stream keeps the facts it started with. The one\n * registration-captured fact \u2014 the retry policy \u2014 re-registers the route in\n * place when it changes.\n *\n * The route is configured pi-ai-style, as a per-route profile under\n * `providers.ollama-cloud-direct`: with no stored profile the route is\n * **dormant on configuration surfaces** \u2014 declared in the configurable-provider\n * directory so the Models settings page lists it in the add-provider select \u2014\n * while the adapter itself serves the resolved defaults (schema defaults plus\n * this module's fallbacks) the moment the plugin mounts, so an ambient\n * `OLLAMA_CLOUD_API_KEY` keeps working before the page ever writes a profile.\n * A model-discovery registration answers the page's fetch action from the\n * resolved catalog, or interrogates a drafted endpoint.\n *\n * Dependencies are intentionally minimal \u2014 `@deepseek-ai/dsh-llm` (the harness\n * LLM seam contract), `@deepseek-ai/dsh-credentials` (the credential seam),\n * `@deepseek-ai/dsh-settings` (the settings-section install), `@deepseek-ai/\n * schemastery` (the section schema), `@deepseek-ai/cordis` (plugin framework),\n * and `eventsource-parser` (SSE framing); validation beyond the schema is\n * hand-rolled. The route is `ollama-cloud-direct` (not `ollama-cloud`) so it\n * can coexist with a pi-ai-configured `ollama-cloud` route.\n *\n * @module llm-ollama-cloud\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport z from '@deepseek-ai/schemastery'\nimport { assertUsableApiKey, LlmError, resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'\nimport type { ModelModality, RetryPolicyConfig } from '@deepseek-ai/dsh-llm'\nimport { credentialRef } from '@deepseek-ai/dsh-credentials'\nimport { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'\nimport {\n DEFAULT_CONTEXT_WINDOW,\n DEFAULT_MAX_TOKENS,\n DEFAULT_STREAM_IDLE_TIMEOUT_MS,\n MAX_TIMER_DELAY_MS,\n normalizeCloud,\n OllamaAdapter,\n} from './adapter.ts'\nimport type { OllamaCatalogModel, OllamaConnectionOptions } from './adapter.ts'\nimport { discoverModels } from './discovery.ts'\n\nexport {\n DEFAULT_CONTEXT_WINDOW,\n DEFAULT_MAX_TOKENS,\n DEFAULT_STREAM_IDLE_TIMEOUT_MS,\n MAX_TIMER_DELAY_MS,\n normalizeCloud,\n OllamaAdapter,\n} from './adapter.ts'\nexport type { OllamaAdapterOptions, OllamaCatalogModel, OllamaConnectionOptions } from './adapter.ts'\nexport { discoverModels } from './discovery.ts'\nexport type { RequestDefaults } from './serialize.ts'\nexport type * from './types.ts'\n\nexport const name = 'llm-ollama-cloud'\nexport const inject = ['llm']\n\nconst NS = settingsNamespace('llm-ollama-cloud')\nconst DEFAULT_API_KEY_ENV = 'OLLAMA_CLOUD_API_KEY'\n/** The single provider route this plugin owns. */\nexport const PROVIDER = 'ollama-cloud-direct'\n\nconst DEFAULT_MODELS: OllamaCatalogModel[] = [\n { id: 'deepseek-v4-flash:cloud', name: 'DeepSeek-V4-Flash (cloud)', contextWindow: DEFAULT_CONTEXT_WINDOW },\n { id: 'deepseek-v4-pro:cloud', name: 'DeepSeek-V4-Pro (cloud)', contextWindow: DEFAULT_CONTEXT_WINDOW },\n { id: 'glm-5.2:cloud', name: 'GLM-5.2 (cloud)', contextWindow: DEFAULT_CONTEXT_WINDOW },\n]\n\nconst MODEL_MODALITIES = ['text', 'image'] as const satisfies readonly ModelModality[]\n\n/**\n * One route's stored profile \u2014 the plugin config's per-route entry and the\n * shape the Models page writes under `providers.<route>`. Every field is\n * optional: a profile naming no reference resolves key material through\n * {@link OllamaProviderProfile.apiKeyEnv}'s default at each request, omitted\n * thinking mode uses the provider default, and omitted reasoning effort lets\n * the server auto-enable thinking at its default.\n */\nexport interface OllamaProviderProfile {\n /** Credential reference (environment-variable name) resolved per request; defaults to `OLLAMA_CLOUD_API_KEY`. */\n apiKeyEnv?: string\n /** Endpoint base; defaults to the Ollama cloud API. */\n baseURL?: string\n /** Deployment thinking policy; `disabled` limits every conversation request to `none` effort. */\n thinking?: 'enabled' | 'disabled'\n /** Default thinking effort (default unset, so the server picks); `off` maps to wire `none`. */\n reasoningEffort?: 'off' | 'low' | 'high' | 'max'\n /** Default per-request output cap (default 65,536); a model's own cap and explicit request values win. */\n maxTokens?: number\n /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */\n defaultContextWindow?: number\n /** Advisory models shown by discovery consumers; a missing `:cloud` suffix is appended. */\n models?: OllamaCatalogModel[]\n /** Maximum provider idle time while one stream read is outstanding (default five minutes). */\n streamIdleTimeoutMs?: number\n /** Provider-owned model-request retry policy; omission uses normal mode with five retries. */\n retryPolicy?: RetryPolicyConfig\n}\n\n/**\n * Plugin config from the `cordis.yml` mount entry, validated by the\n * same-named schemastery schema and doubling as the `llm-ollama-cloud`\n * settings-section shape. Profiles are keyed by provider route id; the route\n * this plugin serves is {@link PROVIDER}. A mount that pins the profile\n * presents the route as configured; a bare mount leaves it dormant in the\n * add-provider select until the page (or `settings.yaml`) writes one.\n */\nexport interface Config {\n /** Per-route profiles keyed by provider route id. */\n providers?: Record<string, OllamaProviderProfile>\n}\n\n/** The catalog-model entry schema (one profile's `models` row). */\nconst catalogModel: z<OllamaCatalogModel> = z.object({\n id: z.string().required(),\n name: z.string(),\n description: z.string(),\n contextWindow: z.number().step(1).min(1),\n maxTokens: z.number().step(1).min(1),\n inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(['text']),\n})\n\n/** One stored route profile; its defaults apply only once the profile exists. */\nconst profileSchema: z<OllamaProviderProfile> = z.object({\n apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),\n baseURL: z.string(),\n thinking: z.union(['enabled', 'disabled']),\n reasoningEffort: z.union(['off', 'low', 'high', 'max']),\n maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS),\n defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),\n models: z.array(catalogModel).default(DEFAULT_MODELS),\n streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),\n retryPolicy: RetryPolicySchema,\n})\n\n/** The `llm-ollama-cloud` settings-section schema; `Config` is its static type. */\nexport const Config: z<Config> = z.object({\n providers: z.dict(profileSchema).default({}),\n})\n\n/** The public Ollama cloud API base. */\nexport const PUBLIC_BASE_URL = 'https://ollama.com/v1'\n\n/** Resolve, validate, and detach the advisory model catalog, normalizing every id to cloud naming. */\nfunction resolveModels(models: readonly OllamaCatalogModel[] | undefined): OllamaCatalogModel[] {\n const seen = new Set<string>()\n return (models ?? DEFAULT_MODELS).map((model) => {\n if (model.id.length === 0) throw new Error('llm-ollama-cloud: catalog model ids must be non-empty')\n const id = normalizeCloud(model.id)\n if (model.name !== undefined && model.name.length === 0) {\n throw new Error(`llm-ollama-cloud: catalog model \"${id}\" has an empty name`)\n }\n if (model.contextWindow !== undefined\n && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) {\n throw new Error(\n `llm-ollama-cloud: catalog model \"${id}\" contextWindow must be a positive integer`,\n )\n }\n if (model.maxTokens !== undefined\n && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {\n throw new Error(\n `llm-ollama-cloud: catalog model \"${id}\" maxTokens must be a positive integer`,\n )\n }\n const inputModalities = model.inputModalities ?? ['text']\n if (inputModalities.length === 0) {\n throw new Error(`llm-ollama-cloud: catalog model \"${id}\" inputModalities must not be empty`)\n }\n if (inputModalities.some(modality => !MODEL_MODALITIES.includes(modality))) {\n throw new Error(\n `llm-ollama-cloud: catalog model \"${id}\" inputModalities must contain only \"text\" and \"image\"`,\n )\n }\n if (new Set(inputModalities).size !== inputModalities.length) {\n throw new Error(`llm-ollama-cloud: catalog model \"${id}\" inputModalities must not contain duplicates`)\n }\n if (seen.has(id)) throw new Error(`llm-ollama-cloud: duplicate catalog model \"${id}\"`)\n seen.add(id)\n return {\n id,\n ...model.name === undefined ? {} : { name: model.name },\n ...model.description === undefined ? {} : { description: model.description },\n ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },\n ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },\n inputModalities: [...inputModalities],\n }\n })\n}\n\n/**\n * The one explicit resolve step from raw config to validated connection\n * facts, with every default and bound re-judged here (fail loud at load).\n * Programmatic construction may bypass Schemastery normalization, so this\n * also re-judges each settings snapshot at its first use.\n * @param config - raw plugin config or resolved settings snapshot.\n * @returns validated connection facts for {@link PROVIDER}.\n */\nexport function resolveAdapterOptions(config: Config): OllamaConnectionOptions {\n return resolveProfileOptions(config.providers?.[PROVIDER])\n}\n\n/**\n * Resolve one raw profile into validated connection facts. A missing profile\n * resolves the defaults, which is the dormant route's serving posture.\n * @param profile - raw profile fields, or `undefined` when none is stored.\n * @returns validated connection facts plus the credential reference.\n */\nexport function resolveProfileOptions(profile: OllamaProviderProfile | undefined): OllamaConnectionOptions {\n if (profile?.thinking === 'disabled'\n && profile.reasoningEffort !== undefined\n && profile.reasoningEffort !== 'off') {\n throw new Error('llm-ollama-cloud: only reasoningEffort \"off\" can be configured when thinking is disabled')\n }\n if (profile?.defaultContextWindow !== undefined\n && (!Number.isInteger(profile.defaultContextWindow) || profile.defaultContextWindow <= 0)) {\n throw new Error('llm-ollama-cloud: defaultContextWindow must be a positive integer')\n }\n if (profile?.maxTokens !== undefined\n && (!Number.isSafeInteger(profile.maxTokens) || profile.maxTokens <= 0)) {\n throw new Error('llm-ollama-cloud: maxTokens must be a positive safe integer')\n }\n const streamIdleTimeoutMs = profile?.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS\n if (!Number.isFinite(streamIdleTimeoutMs)\n || streamIdleTimeoutMs <= 0\n || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) {\n throw new Error(\n `llm-ollama-cloud: streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`,\n )\n }\n return {\n apiKeyEnv: credentialRef(profile?.apiKeyEnv ?? DEFAULT_API_KEY_ENV),\n baseURL: profile?.baseURL ?? PUBLIC_BASE_URL,\n defaults: {\n thinking: profile?.thinking,\n reasoningEffort: profile?.reasoningEffort,\n },\n maxTokens: profile?.maxTokens ?? DEFAULT_MAX_TOKENS,\n defaultContextWindow: profile?.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,\n models: resolveModels(profile?.models),\n streamIdleTimeoutMs,\n retryPolicy: resolveRetryPolicy(profile?.retryPolicy, 'llm-ollama-cloud: retryPolicy'),\n }\n}\n\n/** The `ctx.credentials` service surface this plugin uses (dsh-credentials). */\ninterface CredentialsLike {\n resolve(ref: string): Promise<{ value: string; source: string } | undefined>\n}\n\nexport function apply(ctx: Context, config: Config = {}): void {\n let current: () => Config = () => config\n let lastRaw: Config | undefined\n let lastGood: OllamaConnectionOptions | undefined\n const options = (): OllamaConnectionOptions => {\n const raw = current()\n if (raw === lastRaw && lastGood !== undefined) return lastGood\n try {\n const next = resolveAdapterOptions(raw)\n lastRaw = raw\n lastGood = next\n return next\n } catch (error) {\n // Static composition resolves before anything registers, so this branch\n // only sees a live settings snapshot failing a beyond-schema bound:\n // keep serving the last good facts and say so once per bad snapshot.\n if (lastGood === undefined) throw error\n lastRaw = raw\n ctx.logger.error('llm-ollama-cloud: keeping the last good configuration after an invalid settings section')\n ctx.logger.error(error)\n return lastGood\n }\n }\n options()\n\n const resolveApiKey = async (connection: OllamaConnectionOptions): Promise<string> => {\n // Every credential fact comes from the caller's snapshot, so a rejected\n // settings generation cannot leak its key onto the previous endpoint.\n const ref = connection.apiKeyEnv\n const credentials = ctx.get('credentials') as CredentialsLike | undefined\n if (credentials !== undefined) {\n const hit = await credentials.resolve(ref)\n if (hit !== undefined && hit.value.length > 0) {\n return assertUsableApiKey(hit.value, 'llm-ollama-cloud', ref)\n }\n }\n const ambient = process.env[ref]\n if (ambient !== undefined && ambient.length > 0) {\n return assertUsableApiKey(ambient, 'llm-ollama-cloud', ref)\n }\n throw new LlmError(\n `llm-ollama-cloud: no API key for provider route \"${PROVIDER}\"; store ${ref} through the credentials`\n + ` service (the web Models page writes it), or export ${ref} in the launching environment`,\n 'MISSING_CREDENTIAL',\n )\n }\n /**\n * The stored credential, for a probe whose draft carries none. Missing is\n * an answer here (`undefined`, probe unauthenticated), not a failure \u2014 the\n * request path owns the loud MISSING_CREDENTIAL refusal.\n */\n const storedApiKey = async (): Promise<string | undefined> => {\n const ref = options().apiKeyEnv\n const credentials = ctx.get('credentials') as CredentialsLike | undefined\n const hit = credentials !== undefined ? (await credentials.resolve(ref))?.value : undefined\n const value = hit !== undefined && hit.length > 0 ? hit : process.env[ref]\n return value !== undefined && value.length > 0 ? value : undefined\n }\n\n const adapter = new OllamaAdapter({ options, resolveApiKey })\n // Declared even while dormant, so configuration surfaces list the route in\n // the add-provider select before any profile exists.\n ctx.llm.registerConfigurableProviders([\n { provider: PROVIDER, displayName: 'Ollama Cloud', settingsNs: NS, settingsPath: ['providers', PROVIDER] },\n ])\n // Route effects bind to this apply fiber via the stable `ctx` reference,\n // even when a swap runs inside the scoped settings callback below.\n const registration = ctx.llm.registerAdapter([PROVIDER], adapter)\n let registeredPolicy = options().retryPolicy\n const ensureRegistrationFacts = (): void => {\n const policy = options().retryPolicy\n if (deepEqualJson(policy, registeredPolicy)) return\n // The registry captures the retry policy at registration, so it is the one\n // fact per-request resolution cannot refresh. `replace` re-reads it in one\n // synchronous registry section: disposing and re-registering instead would\n // publish an empty route set between the two, and an observer that reacted\n // to it would see this provider disappear and come back.\n registration.replace([PROVIDER])\n registeredPolicy = policy\n }\n // The Models page's fetch action: a draft naming this route answers from the\n // resolved catalog; anything else is interrogated at the endpoint it shows.\n ctx.llm.registerModelDiscovery(NS, (request, signal) => discoverModels(\n { ...request, ...signal === undefined ? {} : { signal } },\n options().models,\n storedApiKey,\n ))\n installSettingsSection(ctx, NS, Config, config, {\n setSource: (source) => {\n current = source\n },\n onChange: ensureRegistrationFacts,\n })\n}\n", "/**\n * `OllamaAdapter`: fetch + SSE against an Ollama (OpenAI-compatible)\n * chat-completions endpoint, emitting harness StreamChunks. Transport-only:\n * connection facts arrive through a thunk resolved once per operation and the\n * bearer token through a per-request resolver.\n *\n * Model ids are normalized to Ollama's `:cloud` naming on every operation: a\n * request for `deepseek-v4-flash` is sent as `deepseek-v4-flash:cloud`, and an\n * already-suffixed id is forwarded unchanged.\n *\n * Dependencies are intentionally minimal: `@deepseek-ai/dsh-llm` (the harness\n * LLM seam contract), `@deepseek-ai/cordis` (plugin framework), and\n * `eventsource-parser` (SSE framing). Everything else is hand-rolled here.\n *\n * @module llm-ollama-cloud/adapter\n */\n\nimport {\n attributionHeaders,\n contentHasImage,\n CONTEXT_WINDOW_EXCEEDED_CODE,\n isContextWindowExceededError,\n isQuotaExceededError,\n LlmAdapter,\n LlmError,\n ProviderRequestId,\n QUOTA_EXCEEDED_CODE,\n ReasoningEffortId,\n} from '@deepseek-ai/dsh-llm'\nimport type {\n GenerateOptions,\n LlmModelInfo,\n LlmProviderInfo,\n LlmResolvedModelInfo,\n ModelModality,\n ResolvedRetryPolicy,\n StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { serializeRequest } from './serialize.ts'\nimport type { RequestDefaults } from './serialize.ts'\nimport { parseSse } from './sse.ts'\nimport { translate } from './translate.ts'\nimport type { WireError } from './types.ts'\n\n/** One optional model entry advertised by the direct-fetch adapter. */\nexport interface OllamaCatalogModel {\n /** Wire model id accepted by the configured endpoint; a missing `:cloud` suffix is appended. */\n id: string\n /** Selector label; defaults to {@link id}. */\n name?: string\n /** Optional selector detail for deployments with similar model variants. */\n description?: string\n /** Known combined request/response context capacity; omitted when deployment metadata is unavailable. */\n contextWindow?: number\n /** Per-request output cap for this model; omission falls back to the profile's {@link OllamaConnectionOptions.maxTokens}. */\n maxTokens?: number\n /** Accepted request modalities; omission is text-only. */\n inputModalities?: ModelModality[]\n}\n\n/**\n * Validated connection facts for one operation. The plugin's\n * `resolveAdapterOptions` is the one explicit resolve step producing this\n * shape; the adapter trusts it and re-reads it per operation.\n */\nexport interface OllamaConnectionOptions {\n /** Endpoint base; `/chat/completions` is appended. */\n baseURL: string\n /** Environment-variable name holding the bearer token, resolved per request. */\n apiKeyEnv: string\n /** Request defaults applied to every call (thinking mode, effort). */\n defaults: RequestDefaults\n /** Default per-request output cap; explicit request values win. */\n maxTokens: number\n /** Positive context capacity used when the selected model has no exact value. */\n defaultContextWindow: number\n /** Advisory models exposed to discovery consumers; requests remain unrestricted. */\n models: readonly OllamaCatalogModel[]\n /** Maximum provider idle time while one stream read is outstanding. */\n streamIdleTimeoutMs: number\n /** Provider-owned model-request retry policy, already resolved. */\n retryPolicy: ResolvedRetryPolicy\n}\n\n/** Constructor options for {@link OllamaAdapter}: the operation-local resolution hooks the plugin owns. */\nexport interface OllamaAdapterOptions {\n /** Current validated connection facts; called once per operation. */\n options: () => OllamaConnectionOptions\n /**\n * Resolve the bearer token for the connection facts of one request. The\n * snapshot is passed in \u2014 never re-read \u2014 so the key can only ever come\n * from the same resolution as the endpoint it is sent to.\n */\n resolveApiKey: (connection: OllamaConnectionOptions) => Promise<string>\n}\n\n/** Default maximum idle interval while an adapter stream read is outstanding. */\nexport const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000\n/** Default combined request/response context capacity. */\nexport const DEFAULT_CONTEXT_WINDOW = 1_000_000\n/** Default per-request output-token cap. */\nexport const DEFAULT_MAX_TOKENS = 65_536\n/** The Ollama cloud model-name suffix this adapter appends when missing. */\nexport const CLOUD_SUFFIX = ':cloud'\n/** Largest value `setTimeout` accepts (2^31 - 1 ms). */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647\nconst STREAM_IDLE_TIMEOUT_CODE = 'LLM_STREAM_IDLE_TIMEOUT'\nconst OFF_REASONING_EFFORT = ReasoningEffortId('off')\nconst LOW_REASONING_EFFORT = ReasoningEffortId('low')\nconst HIGH_REASONING_EFFORT = ReasoningEffortId('high')\nconst MAX_REASONING_EFFORT = ReasoningEffortId('max')\nconst REASONING_EFFORTS = [\n { id: OFF_REASONING_EFFORT, name: 'Off' },\n { id: LOW_REASONING_EFFORT, name: 'Low' },\n { id: HIGH_REASONING_EFFORT, name: 'High' },\n { id: MAX_REASONING_EFFORT, name: 'Max' },\n] as const\nconst OFF_ONLY_REASONING_EFFORTS = [\n { id: OFF_REASONING_EFFORT, name: 'Off' },\n] as const\n\n/**\n * Normalize a model id to Ollama's cloud naming. An id already carrying the\n * `:cloud` suffix is returned unchanged; any other id gets it appended. This\n * is the one place a bare harness model name becomes a wire model name.\n * @param model - the requested model id.\n * @returns the id with a `:cloud` suffix.\n */\nexport function normalizeCloud(model: string): string {\n return model.endsWith(CLOUD_SUFFIX) ? model : `${model}${CLOUD_SUFFIX}`\n}\n\n/**\n * Minimal idle watchdog: arms a timer on construction and after every read,\n * and aborts its signal when the idle budget elapses without a pulse. The\n * {@link OllamaAdapter} maps the expired flag to `TIMEOUT` and the caller's\n * own abort to `ABORTED`.\n */\nclass IdleWatchdog {\n private readonly controller = new AbortController()\n private timer: ReturnType<typeof setTimeout> | undefined\n private expired = false\n /** Combined caller + watchdog signal; aborts when either fires. */\n readonly signal: AbortSignal\n\n constructor(upstream: AbortSignal, private readonly timeoutMs: number) {\n this.signal = upstream.aborted\n ? upstream\n : AbortSignal.any([upstream, this.controller.signal])\n if (!upstream.aborted) {\n upstream.addEventListener('abort', () => this.stop(), { once: true })\n }\n }\n\n get didExpire(): boolean {\n return this.expired\n }\n\n private arm(): void {\n this.stop()\n this.timer = setTimeout(() => {\n this.expired = true\n this.controller.abort(new Error(STREAM_IDLE_TIMEOUT_CODE))\n }, this.timeoutMs)\n }\n\n /** Rearm the idle window; called after each provider read. */\n pulse(): void {\n this.arm()\n }\n\n stop(): void {\n if (this.timer !== undefined) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n }\n}\n\nfunction modelInfo(provider: string, model: OllamaCatalogModel): LlmModelInfo {\n return {\n provider,\n id: model.id,\n name: model.name ?? model.id,\n ...model.description === undefined ? {} : { description: model.description },\n inputModalities: model.inputModalities ?? ['text'],\n }\n}\n\nfunction providerRetryAfterMs(value: string | null): number | undefined {\n if (value === null) return undefined\n if (/^\\d+$/.test(value)) {\n const delay = Number(value) * 1_000\n return Number.isFinite(delay) && delay > 0 ? delay : undefined\n }\n const delay = Date.parse(value) - Date.now()\n return Number.isFinite(delay) && delay > 0 ? delay : undefined\n}\n\nfunction requestId(headers: Headers): ReturnType<typeof ProviderRequestId> | undefined {\n const value = headers.get('x-request-id') ?? headers.get('x-ollama-request-id')\n return value === null || value.length === 0 ? undefined : ProviderRequestId(value)\n}\n\n/**\n * Map an HTTP status to a stable LlmError code.\n * @param status - status of a non-2xx provider response.\n * @param error - parsed provider error body, when available.\n * @returns the normalized harness error code.\n */\nexport function httpErrorCode(status: number, error?: WireError['error']): string {\n if (status === 401 || status === 403) return 'AUTH'\n if (status === 413) return 'INVALID_REQUEST'\n const detail = [error?.code, error?.type, error?.message].filter(Boolean).join(' ')\n if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE\n if (status === 429) return 'RATE_LIMIT'\n if (status === 400) {\n if (isContextWindowExceededError(detail)) return CONTEXT_WINDOW_EXCEEDED_CODE\n return 'INVALID_REQUEST'\n }\n if (status >= 500) return 'SERVER'\n return `HTTP_${status}`\n}\n\n/**\n * One instance serves every model name it was registered under. The harness\n * model name is normalized to its cloud form and IS the wire model name.\n *\n * One stable signal reaches both initial fetch and body reads. Caller aborts\n * map to `ABORTED`; the configured per-read idle watchdog maps to `TIMEOUT`.\n */\nexport class OllamaAdapter extends LlmAdapter {\n constructor(private readonly config: OllamaAdapterOptions) {\n super()\n }\n\n override providerInfo(provider: string): LlmProviderInfo {\n // Matches the configurable-provider directory's displayName, so the\n // Models page row and the model-picker group name read as one provider.\n return { id: provider, name: 'Ollama Cloud' }\n }\n\n override providerRetryPolicy(_provider: string): ResolvedRetryPolicy {\n return this.config.options().retryPolicy\n }\n\n override listModels(provider: string): Promise<readonly LlmModelInfo[]> {\n return Promise.resolve(this.config.options().models.map(model => modelInfo(provider, model)))\n }\n\n override resolveModel(\n provider: string,\n model: string,\n _signal?: AbortSignal,\n ): Promise<LlmResolvedModelInfo> {\n const connection = this.config.options()\n // Resolve against the wire (cloud-suffixed) id so an unsuffixed request\n // still matches its catalog entry and reports the cloud id onward.\n const wireModel = normalizeCloud(model)\n const configured = connection.models.find(entry => entry.id === wireModel)\n const contextWindow = configured?.contextWindow\n ?? connection.defaultContextWindow\n return Promise.resolve({\n // An uncatalogued endpoint is safely treated as text-only.\n ...configured === undefined\n ? { provider, id: wireModel, name: wireModel, inputModalities: ['text' as const] }\n : modelInfo(provider, configured),\n context: { contextWindow },\n defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,\n ...connection.defaults.thinking === 'disabled'\n ? {\n reasoning: {\n efforts: OFF_ONLY_REASONING_EFFORTS,\n defaultEffort: OFF_REASONING_EFFORT,\n },\n }\n : {\n reasoning: {\n efforts: REASONING_EFFORTS,\n defaultEffort: connection.defaults.reasoningEffort === 'off'\n ? OFF_REASONING_EFFORT\n : connection.defaults.reasoningEffort === 'low'\n ? LOW_REASONING_EFFORT\n : connection.defaults.reasoningEffort === 'max'\n ? MAX_REASONING_EFFORT\n : HIGH_REASONING_EFFORT,\n },\n },\n })\n }\n\n async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {\n // One resolution per stream call: connection facts and the credential\n // freeze here and hold for this whole request.\n const connection = this.config.options()\n if (options.messages.some(message => contentHasImage(message.content))) {\n throw new LlmError(\n 'Ollama image input is not supported yet.',\n 'UNSUPPORTED_CONTENT',\n )\n }\n const apiKey = await this.config.resolveApiKey(connection)\n const consumer = new AbortController()\n const upstream = options.signal === undefined\n ? consumer.signal\n : AbortSignal.any([options.signal, consumer.signal])\n const watchdog = new IdleWatchdog(upstream, connection.streamIdleTimeoutMs)\n const iterator = this.request(\n options,\n watchdog.signal,\n connection,\n apiKey,\n () => watchdog.pulse(),\n )[Symbol.asyncIterator]()\n let exhausted = false\n try {\n while (true) {\n watchdog.pulse()\n const result = await iterator.next()\n if (result.done) {\n exhausted = true\n return\n }\n yield result.value\n }\n } catch (error: unknown) {\n if (watchdog.didExpire) {\n throw new LlmError(\n `Ollama stream idle timeout after ${connection.streamIdleTimeoutMs}ms`,\n 'TIMEOUT',\n { cause: error },\n )\n }\n if (options.signal?.aborted) {\n throw new LlmError('Ollama request aborted by caller', 'ABORTED', { cause: error })\n }\n if (error instanceof LlmError) throw error\n throw new LlmError(`Ollama API stream from ${connection.baseURL} failed`, 'TRANSPORT', { cause: error })\n } finally {\n watchdog.stop()\n consumer.abort('Ollama stream consumer stopped')\n if (!exhausted && iterator.return !== undefined) {\n try {\n await iterator.return()\n } catch (_abortedTransportTeardown) {\n // The consumer controller already owns termination; a return-time abort cannot add a second outcome.\n }\n }\n }\n }\n\n private async * request(\n options: GenerateOptions,\n signal: AbortSignal,\n connection: OllamaConnectionOptions,\n apiKey: string,\n onComment: () => void,\n ): AsyncIterable<StreamChunk> {\n const body = serializeRequest(\n { ...options, model: normalizeCloud(options.model) },\n connection.defaults,\n )\n // Prepared outside the try so the TRANSPORT label below covers exactly the\n // transport boundary, never a serialization failure.\n const payload = JSON.stringify(body)\n const headers = {\n 'authorization': `Bearer ${apiKey}`,\n 'content-type': 'application/json',\n 'accept': 'text/event-stream',\n ...attributionHeaders(),\n ...options.sessionId !== undefined\n ? { 'x-deepseek-harness-session-id': String(options.sessionId) }\n : {},\n ...options.purpose === 'compaction'\n ? { 'x-deepseek-harness-compact': '1' }\n : {},\n }\n\n let response: Response\n try {\n response = await fetch(`${connection.baseURL}/chat/completions`, {\n method: 'POST',\n headers,\n body: payload,\n signal,\n })\n } catch (error: unknown) {\n // The outer stream distinguishes caller cancellation and watchdog expiry.\n if (signal.aborted) throw error\n // fetch wraps every transport failure (DNS, refused connection, TLS,\n // proxy) in a bare `TypeError: fetch failed` whose actionable detail\n // lives on `cause`.\n throw new LlmError(\n `Ollama API request to ${connection.baseURL} failed`,\n 'TRANSPORT',\n { cause: error },\n )\n }\n\n if (!response.ok) {\n let message = `Ollama API error (HTTP ${response.status})`\n let providerError: WireError['error']\n try {\n const parsed = await response.json() as WireError\n providerError = parsed.error\n if (providerError?.message) message = providerError.message\n } catch {\n // Only swallow error-body parsing: the HTTP status still identifies the\n // failure, so malformed gateway JSON must not mask it.\n }\n const delay = providerRetryAfterMs(response.headers.get('retry-after'))\n const id = requestId(response.headers)\n throw new LlmError(message, httpErrorCode(response.status, providerError), {\n status: response.status,\n ...delay === undefined ? {} : { providerRetryAfterMs: delay },\n ...id === undefined ? {} : { requestId: id },\n })\n }\n if (!response.body) {\n throw new LlmError('Ollama API returned no response body', 'EMPTY_RESPONSE')\n }\n\n yield* translate(parseSse(response.body, onComment))\n }\n}\n", "/**\n * Serialize harness messages into an Ollama chat completions request.\n * Text-only (the OpenAI-compatible endpoint's image path is deferred); tool\n * results become standalone `role: 'tool'` messages. Reasoning is replayed as\n * the `reasoning` assistant field only for reasoning-capable models (a wire id\n * containing `deepseek`), so non-reasoning models keep clean traces.\n * @module dsh-llm-ollama-cloud/serialize\n */\n\nimport { contentHasImage, LlmError } from '@deepseek-ai/dsh-llm'\nimport type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'\nimport type {\n WireMessage,\n WireRequest,\n WireTool,\n} from './types.ts'\n\n/** Adapter-level request defaults (from plugin config). */\nexport interface RequestDefaults {\n thinking?: 'enabled' | 'disabled' | undefined\n reasoningEffort?: 'off' | 'low' | 'high' | 'max' | undefined\n}\n\n/** The Ollama reasoning-effort values this adapter emits on the wire. */\nexport type WireReasoningEffort = 'none' | 'low' | 'high' | 'max'\n\ninterface ResolvedThinking {\n reasoningEffort?: WireReasoningEffort\n}\n\n/**\n * Whether a model's reasoning should be passed back on assistant history.\n * Only reasoning-capable models accept the `reasoning` field; a non-reasoning\n * model ignores it, so it is written only for wire ids containing `deepseek`.\n * @param model - the wire model id.\n * @returns true when the model is treated as reasoning-capable.\n */\nexport function passReasoning(model: string): boolean {\n return model.includes('deepseek')\n}\n\n/** Validate the adapter-owned effort before resolving its Ollama wire value. */\nfunction reasoningEffort(effort: NonNullable<GenerateOptions['reasoningEffort']>): 'off' | 'low' | 'high' | 'max' {\n if (effort === 'off' || effort === 'low' || effort === 'high' || effort === 'max') {\n return effort as 'off' | 'low' | 'high' | 'max'\n }\n throw new LlmError(\n `Ollama does not support reasoning effort \"${effort}\"`,\n 'UNSUPPORTED_REASONING_EFFORT',\n )\n}\n\n/**\n * Resolve one legal thinking/effort pair into an Ollama wire effort. An `off`\n * (or a `disabled` deployment default) maps to `none`; an explicit effort maps\n * to its Ollama spelling; an omitted effort with thinking enabled sends\n * nothing so the server auto-enables thinking at its default.\n * @param options - the harness request.\n * @param defaults - adapter-level thinking defaults.\n * @returns the wire `reasoning_effort`, or nothing when the server default should apply.\n */\nfunction resolveThinking(options: GenerateOptions, defaults: RequestDefaults): ResolvedThinking {\n if (options.purpose === 'session-title') return { reasoningEffort: 'none' }\n const effort = options.reasoningEffort === undefined\n ? defaults.reasoningEffort\n : reasoningEffort(options.reasoningEffort)\n if (defaults.thinking === 'disabled' && effort !== undefined && effort !== 'off') {\n throw new LlmError(\n `Ollama deployment does not support reasoning effort \"${effort}\"`,\n 'UNSUPPORTED_REASONING_EFFORT',\n )\n }\n if (effort === 'off') return { reasoningEffort: 'none' }\n if (effort === 'low' || effort === 'high' || effort === 'max') {\n return { reasoningEffort: effort }\n }\n // effort undefined: disabled defaults suppress reasoning, enabled or unset\n // ones send nothing and let the server pick its default.\n return defaults.thinking === 'disabled' ? { reasoningEffort: 'none' } : {}\n}\n\n/** Join the text blocks of a message (used for user/tool-result content). */\nfunction flattenText(blocks: ContentBlock[]): string {\n return blocks\n .filter(block => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\n/** Reject core image content before any text-flattening path can silently erase it. */\nfunction assertTextOnly(blocks: readonly ContentBlock[]): void {\n if (contentHasImage(blocks)) {\n throw new LlmError('The Ollama chat-completions adapter does not support image content yet.', 'UNSUPPORTED_CONTENT')\n }\n}\n\n/** Serialize one assistant message (text + optional reasoning + tool calls). */\nfunction serializeAssistant(message: Message, model: string): WireMessage {\n const text = flattenText(message.content)\n const reasoning = message.content\n .filter(block => block.type === 'reasoning')\n .map(block => block.text)\n .join('')\n const toolCalls = message.content\n .filter(block => block.type === 'tool-call')\n .map(block => ({\n id: block.id,\n type: 'function' as const,\n function: { name: block.name, arguments: block.arguments },\n }))\n\n return {\n role: 'assistant',\n // Text-less turns send \"\" \u2014 NEVER null. Reasoning-only turns (the model\n // can answer entirely in the reasoning channel) risk a gateway 400, and\n // since the message sits durably in the session log, a null here bricks\n // every later turn of that session.\n content: text,\n // CoT passback only for reasoning-capable models, via the `reasoning`\n // field Ollama accepts on assistant history.\n ...passReasoning(model) && reasoning.length > 0 ? { reasoning } : {},\n ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},\n }\n}\n\n/**\n * Serialize the conversation. `tool-result` blocks become standalone\n * `{role: 'tool'}` messages; the harness puts each tool result in its own\n * user-role message, so a mixed user message contributes its text first and\n * its tool results as separate wire messages after.\n * @param model - the wire model id, used to decide reasoning passback.\n * @param messages - the harness conversation, in order.\n * @returns the wire messages; order preserved, each tool result expanded into its own entry.\n */\nexport function serializeMessages(model: string, messages: Message[]): WireMessage[] {\n const wire: WireMessage[] = []\n for (const message of messages) {\n assertTextOnly(message.content)\n if (message.role === 'system') {\n wire.push({ role: 'system', content: flattenText(message.content) })\n continue\n }\n if (message.role === 'assistant') {\n wire.push(serializeAssistant(message, model))\n continue\n }\n // user role: tool results ride in user messages in the harness\n // vocabulary, but Ollama wants them as role:'tool' messages.\n const toolResults = message.content.filter(block => block.type === 'tool-result')\n const text = flattenText(message.content)\n if (text.length > 0 || toolResults.length === 0) {\n wire.push({ role: 'user', content: text })\n }\n for (const result of toolResults) {\n wire.push({\n role: 'tool',\n tool_call_id: result.toolCallId,\n // Empty tool output still needs SOME content on the wire.\n content: flattenText(result.content) || '(no output)',\n })\n }\n }\n return wire\n}\n\n/** Assemble request fields shared by every conversion. */\nfunction requestWithMessages(\n options: GenerateOptions,\n messages: WireMessage[],\n defaults: RequestDefaults,\n): WireRequest {\n const tools: WireTool[] | undefined = options.tools?.map(tool => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters,\n },\n }))\n const resolvedThinking = resolveThinking(options, defaults)\n return {\n model: options.model,\n messages,\n stream: true,\n stream_options: { include_usage: true },\n ...resolvedThinking.reasoningEffort !== undefined\n ? { reasoning_effort: resolvedThinking.reasoningEffort }\n : {},\n ...tools !== undefined && tools.length > 0 ? { tools } : {},\n ...options.temperature !== undefined ? { temperature: options.temperature } : {},\n ...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },\n ...options.stop !== undefined ? { stop: options.stop } : {},\n }\n}\n\n/**\n * Build the full wire request. Always streaming (`stream: true`, usage\n * reporting on); optional fields are omitted rather than sent as null, so\n * provider defaults apply.\n * @param options - the harness request (model, history, system, tools, sampling).\n * @param defaults - adapter-level thinking defaults; undefined fields put nothing on the wire.\n * @returns the chat-completions request body.\n */\nexport function serializeRequest(\n options: GenerateOptions,\n defaults: RequestDefaults = {},\n): WireRequest {\n const messages: WireMessage[] = []\n if (options.system !== undefined) {\n messages.push({ role: 'system', content: options.system })\n }\n messages.push(...serializeMessages(options.model, options.messages))\n\n return requestWithMessages(options, messages, defaults)\n}\n", "/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field' | 'max-buffer-size-exceeded'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n", "/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserConfig} from './types.ts'\n\n// ASCII codes used in the hot parsing paths.\nconst LF = 10\nconst CR = 13\nconst SPACE = 32\n\n// oxlint-disable-next-line no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param config - Parser configuration. Accepts callbacks (see {@link ParserCallbacks})\n * and options like `maxBufferSize` (see {@link ParserConfig}).\n *\n * @returns A new EventSource parser, with `feed` and `reset` methods.\n * @public\n */\nexport function createParser(config: ParserConfig): EventSourceParser {\n if (typeof config === 'function') {\n throw new TypeError(\n '`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment, maxBufferSize} = config\n\n // Trailing bytes from prior `feed()` calls that did not yet form a complete line.\n // Stored as an array of fragments and only joined when a line terminator arrives.\n // Concatenating per-feed (`prefix + chunk`) is O(N²) when a single SSE line spans\n // many chunks (e.g. a large `data:` payload streamed in tiny slices, or an MCP-style\n // server that emits one giant content block). Buffering as fragments + joining once\n // makes the same workload linear.\n const pendingFragments: string[] = []\n\n // Running total of `pendingFragments` lengths, kept in sync with the array so the\n // `maxBufferSize` check doesn't have to walk the fragment list on every feed.\n let pendingFragmentsLength = 0\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let dataLines = 0\n let eventType: string | undefined\n\n // Set after a `maxBufferSize` overflow. Once tripped, `feed()` throws until\n // `reset()` is called — see the comment on `maxBufferSize` in `ParserConfig`.\n let terminated = false\n\n /**\n * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do\n * not yet form a complete line are held back and prepended to the next chunk,\n * so callers can pass arbitrary slices of the stream without worrying about\n * line boundaries.\n *\n * Per the SSE spec, a UTF-8 BOM (0xEF 0xBB 0xBF) at the start of the very\n * first chunk is stripped before parsing.\n *\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream\n */\n function feed(chunk: string) {\n if (terminated) {\n throw new Error(\n 'Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.',\n )\n }\n\n if (isFirstChunk) {\n isFirstChunk = false\n // Match and strip UTF-8 BOM from the start of the stream, if present.\n // (Per the spec, this is only valid at the very start of the stream)\n if (\n chunk.charCodeAt(0) === 0xef &&\n chunk.charCodeAt(1) === 0xbb &&\n chunk.charCodeAt(2) === 0xbf\n ) {\n chunk = chunk.slice(3)\n }\n }\n\n // Hot path: no buffered prefix from a prior partial line. Hand the chunk\n // straight to `processLines`, exactly like the original implementation.\n // Zero new work in the common case (every chunk ends with `\\n\\n`).\n if (pendingFragments.length === 0) {\n const trailing = processLines(chunk)\n if (trailing !== '') {\n pendingFragments.push(trailing)\n pendingFragmentsLength = trailing.length\n }\n checkBufferSize()\n return\n }\n\n // We have a buffered prefix. If this chunk also has no terminator, append\n // to the buffer without concatenating — that's the O(N²) trap we're\n // avoiding (large single `data:` payload split across many tiny chunks).\n if (chunk.indexOf('\\n') === -1 && chunk.indexOf('\\r') === -1) {\n pendingFragments.push(chunk)\n pendingFragmentsLength += chunk.length\n checkBufferSize()\n return\n }\n\n // Terminator arrived. Join the accumulated fragments + this chunk once,\n // process, and buffer any new trailing partial line.\n pendingFragments.push(chunk)\n const input = pendingFragments.join('')\n pendingFragments.length = 0\n pendingFragmentsLength = 0\n const trailing = processLines(input)\n if (trailing !== '') {\n pendingFragments.push(trailing)\n pendingFragmentsLength = trailing.length\n }\n checkBufferSize()\n }\n\n function checkBufferSize() {\n if (maxBufferSize === undefined) return\n if (pendingFragmentsLength + data.length <= maxBufferSize) return\n\n terminated = true\n pendingFragments.length = 0\n pendingFragmentsLength = 0\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n onError(\n new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, {\n type: 'max-buffer-size-exceeded',\n }),\n )\n }\n\n /**\n * Splits `chunk` into SSE lines and dispatches each to the appropriate handler.\n * Returns any trailing bytes that did not terminate with a line break, so the\n * caller can prepend them to the next chunk.\n *\n * The SSE spec permits three line terminators: `\\n`, `\\r`, and `\\r\\n`. Real-world\n * streams almost always use plain `\\n`, so we take a fast path when no `\\r` is\n * present in the chunk. The slow path is spec-correct but does more work per line.\n */\n function processLines(chunk: string): string {\n let searchIndex = 0\n\n // Fast path: LF-only chunk (the common case for typical SSE servers).\n // We can scan forward with a single `indexOf('\\n')` per line and inline\n // the hot-path branches for `data:` and `event:` without the CR bookkeeping\n // the slow path needs.\n if (chunk.indexOf('\\r') === -1) {\n let lfIndex = chunk.indexOf('\\n', searchIndex)\n while (lfIndex !== -1) {\n // Blank line: end-of-event marker. Dispatch the accumulated event (if any)\n // and reset the buffered fields. This is hoisted out of `parseLine` because\n // it's the single most common line shape after `data:` lines.\n if (searchIndex === lfIndex) {\n if (dataLines > 0) {\n onEvent({id, event: eventType, data})\n }\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n const firstCharCode = chunk.charCodeAt(searchIndex)\n if (isDataPrefix(chunk, searchIndex, firstCharCode)) {\n // `data:` line — append the value to the event's data buffer.\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart =\n chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5\n const value = chunk.slice(valueStart, lfIndex)\n // Fast path within a fast path: if this is the first data line AND the\n // next char is another LF (i.e. `data:foo\\n\\n`), dispatch immediately\n // without ever writing to the `data` buffer. This is the shape of a\n // typical single-line SSE event (ChatGPT-style streams, etc.) and is\n // hot enough to be worth the duplication.\n if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {\n onEvent({id, event: eventType, data: value})\n id = undefined\n data = ''\n eventType = undefined\n searchIndex = lfIndex + 2\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n // Multi-line data: concatenate with newline separator per spec.\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {\n // `event:` line — set the event type for the next dispatch. Per spec,\n // an empty value resets `event type` to its default (undefined here).\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(\n chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,\n lfIndex,\n ) || undefined\n } else {\n // Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown\n // fields, or malformed lines. These are rarer and go through the full\n // per-line parser, which handles the SSE field grammar in detail.\n parseLine(chunk, searchIndex, lfIndex)\n }\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n }\n return chunk.slice(searchIndex)\n }\n\n // Slow path: the chunk contains at least one `\\r`, so lines may be terminated\n // by `\\r`, `\\n`, or `\\r\\n`. We locate the next terminator by looking at both\n // the nearest `\\r` and `\\n` and picking whichever comes first.\n while (searchIndex < chunk.length) {\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n lineEnd = crIndex < lfIndex ? crIndex : lfIndex\n } else if (crIndex !== -1) {\n // A trailing `\\r` at the very end of the chunk is ambiguous: it could be\n // a bare-CR terminator, or the first half of a `\\r\\n` whose `\\n` arrives\n // in the next chunk. Defer until we see more input.\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n if (lineEnd === -1) {\n break\n }\n\n parseLine(chunk, searchIndex, lineEnd)\n searchIndex = lineEnd + 1\n // If we just consumed a `\\r` and the next char is `\\n`, skip it so the\n // pair is treated as a single terminator rather than an empty line.\n if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {\n searchIndex++\n }\n }\n\n return chunk.slice(searchIndex)\n }\n\n function parseLine(chunk: string, start: number, end: number) {\n if (start === end) {\n dispatchEvent()\n return\n }\n\n const firstCharCode = chunk.charCodeAt(start)\n\n if (isDataPrefix(chunk, start, firstCharCode)) {\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5\n const value = chunk.slice(valueStart, end)\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n return\n }\n\n if (isEventPrefix(chunk, start, firstCharCode)) {\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined\n return\n }\n\n // Fast path for \"id:\" — 'i' = 105, 'd' = 100, ':' = 58\n if (\n firstCharCode === 105 &&\n chunk.charCodeAt(start + 1) === 100 &&\n chunk.charCodeAt(start + 2) === 58\n ) {\n // 'id:'.length === 3, 'id: '.length === 4\n const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end)\n id = value.includes('\\0') ? undefined : value\n return\n }\n\n // Comment line — ':' = 58\n if (firstCharCode === 58) {\n if (onComment) {\n const line = chunk.slice(start, end)\n // skip ':' (+1), or ': ' (+2) when a space follows\n onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1))\n }\n return\n }\n\n const line = chunk.slice(start, end)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex === -1) {\n processField(line, '', line)\n return\n }\n\n const field = line.slice(0, fieldSeparatorIndex)\n // skip ':' (+1), or ': ' (+2) when a space follows\n const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n processField(field, value, line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value || undefined\n break\n case 'data':\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n if (dataLines > 0) {\n onEvent({\n id,\n event: eventType,\n data,\n })\n }\n\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (options.consume && pendingFragments.length > 0) {\n const incompleteLine = pendingFragments.join('')\n parseLine(incompleteLine, 0, incompleteLine.length)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n pendingFragments.length = 0\n pendingFragmentsLength = 0\n terminated = false\n }\n\n return {feed, reset}\n}\n\n/**\n * Checks if `chunk` starts with the literal `data:` at index `i`.\n *\n * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this\n * hand-unrolled char-code comparison is ~20% faster on common event types.\n * The caller passes `firstCharCode` (the code at `i`) so it can be reused\n * across prefix checks.\n *\n * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58\n */\nfunction isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 100 &&\n chunk.charCodeAt(i + 1) === 97 &&\n chunk.charCodeAt(i + 2) === 116 &&\n chunk.charCodeAt(i + 3) === 97 &&\n chunk.charCodeAt(i + 4) === 58\n )\n}\n\n/**\n * Checks if `chunk` starts with the literal `event:` at index `i`.\n *\n * See {@link isDataPrefix} for why this is hand-unrolled rather than using\n * `String.prototype.startsWith`.\n *\n * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58\n */\nfunction isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 101 &&\n chunk.charCodeAt(i + 1) === 118 &&\n chunk.charCodeAt(i + 2) === 101 &&\n chunk.charCodeAt(i + 3) === 110 &&\n chunk.charCodeAt(i + 4) === 116 &&\n chunk.charCodeAt(i + 5) === 58\n )\n}\n", "import {createParser} from './parse.ts'\nimport type {EventSourceMessage, EventSourceParser} from './types.ts'\n\n/**\n * Options for the EventSourceParserStream.\n *\n * @public\n */\nexport interface StreamOptions {\n /**\n * Behavior when a parsing error occurs.\n *\n * - A custom function can be provided to handle the error.\n * - `'terminate'` will error the stream and stop parsing.\n * - Any other value will ignore the error and continue parsing.\n *\n * @defaultValue `undefined`\n */\n onError?: ('terminate' | ((error: Error) => void)) | undefined\n\n /**\n * Callback for when a reconnection interval is sent from the server.\n *\n * @param retry - The number of milliseconds to wait before reconnecting.\n */\n onRetry?: ((retry: number) => void) | undefined\n\n /**\n * Callback for when a comment is encountered in the stream.\n *\n * @param comment - The comment encountered in the stream.\n */\n onComment?: ((comment: string) => void) | undefined\n\n /**\n * Maximum number of characters the parser is allowed to buffer across calls to `feed()`.\n * See {@link ParserConfig.maxBufferSize} for details.\n *\n * When the limit is exceeded, the stream is always errored (regardless of the `onError`\n * setting) since the underlying parser is unrecoverable without a `reset()`.\n *\n * @defaultValue `undefined` (unbounded)\n */\n maxBufferSize?: number | undefined\n}\n\n/**\n * A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.\n *\n * @example Basic usage\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream())\n * ```\n *\n * @example Terminate stream on parsing errors\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream({onError: 'terminate'}))\n * ```\n *\n * @public\n */\nexport class EventSourceParserStream extends TransformStream<string, EventSourceMessage> {\n constructor({onError, onRetry, onComment, maxBufferSize}: StreamOptions = {}) {\n let parser!: EventSourceParser\n\n super({\n start(controller) {\n parser = createParser({\n onEvent: (event) => {\n controller.enqueue(event)\n },\n onError(error) {\n if (typeof onError === 'function') {\n onError(error)\n }\n\n // `max-buffer-size-exceeded` is fatal — the parser is unusable until\n // `reset()`, which the stream wrapper has no way to call. Always\n // terminate the stream in that case so consumers see the meaningful\n // `ParseError` instead of an opaque \"cannot feed terminated parser\"\n // throw from the next chunk.\n if (onError === 'terminate' || error.type === 'max-buffer-size-exceeded') {\n controller.error(error)\n }\n\n // Ignore by default\n },\n onRetry,\n onComment,\n maxBufferSize,\n })\n },\n transform(chunk) {\n parser.feed(chunk)\n },\n })\n }\n}\n\nexport {type ErrorType, ParseError} from './errors.ts'\nexport type {EventSourceMessage} from './types.ts'\n", "/**\n * Decode an SSE byte stream into event `data` payloads. Framing \u2014 chunk\n * reassembly, UTF-8/CRLF/BOM handling, comment and non-data field skipping,\n * multi-`data:` joining \u2014 is `eventsource-parser`'s. Comments are reported\n * only through an optional transport-activity callback. This module keeps the\n * OpenAI-compatible protocol: the literal `[DONE]` is yielded so the caller\n * owns final flushing, and EOF before it raises {@link LlmError}. Framing is\n * spec-strict: an event dispatches only on its blank-line terminator, so an\n * unterminated tail at EOF is truncation, not a flushable payload.\n *\n * @module dsh-llm-ollama-cloud/sse\n */\n\nimport { EventSourceParserStream } from 'eventsource-parser/stream'\nimport { LlmError } from '@deepseek-ai/dsh-llm'\n\n/** The terminal payload OpenAI-compatible endpoints send after the last chunk. */\nexport const DONE = '[DONE]'\n\n/**\n * Parse an SSE byte stream into data payloads. Yields `[DONE]` as the final\n * value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends\n * without it (truncated response \u2014 the model call cannot be trusted).\n * @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence.\n * @param onComment - optional transport-activity callback; comments never enter the yielded payload stream.\n * @returns each event's data payload in arrival order, the `[DONE]` sentinel last.\n */\nexport async function* parseSse(\n stream: ReadableStream<BufferSource>,\n onComment?: (comment: string) => void,\n): AsyncGenerator<string> {\n const events = stream\n .pipeThrough(new TextDecoderStream())\n .pipeThrough(new EventSourceParserStream({ onComment }))\n for await (const { data } of events) {\n yield data\n if (data === DONE) return\n }\n throw new LlmError('SSE stream ended without [DONE]', 'STREAM_CLOSED')\n}\n", "/**\n * Translate Ollama wire chunks into the harness `StreamChunk` protocol with\n * one stateful harness block per content, reasoning, or tool call index. An\n * empty initial reasoning delta does not open a block. Finish reason and the\n * latest usage are deferred until `[DONE]`, covering both finish-attached and\n * trailing usage-only shapes while ensuring no chunk follows `finish`.\n * @module dsh-llm-ollama-cloud/translate\n */\n\nimport { EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm'\nimport type { ContentBlock, FinishReason, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm'\nimport type { Branded } from '@deepseek-ai/dsh-brand'\nimport { DONE } from './sse.ts'\nimport type { WireChunk, WireUsage } from './types.ts'\n\n/**\n * Brand a provider-issued tool call id. dsh-llm's export name differs across\n * harness versions (`ToolCallId` vs `CallId`), so the plugin brands locally\n * with the shared dsh-brand primitive; the brand is type-only, and the runtime\n * value is the plain string either version accepts.\n */\ntype ToolCallId = Branded<'ToolCallId'>\nfunction ToolCallId(id: string): ToolCallId {\n return id as ToolCallId\n}\n\n/** One open block under assembly. */\ninterface OpenBlock {\n index: number\n kind: 'text' | 'reasoning' | 'tool-call'\n text: string\n /** tool-call only */\n callId?: string\n name?: string\n}\n\n/**\n * Map the wire finish_reason vocabulary to the harness FinishReason.\n * @param reason - the wire `finish_reason` string.\n * @returns the mapped reason; unrecognized values (content_filter, \u2026) become `{kind: 'error'}` with the uppercased value as `code`.\n */\nexport function mapFinishReason(reason: string): FinishReason {\n switch (reason) {\n case 'stop': return { kind: 'stop' }\n case 'tool_calls': return { kind: 'tool-calls' }\n case 'length': return { kind: 'max-tokens' }\n default:\n // content_filter, insufficient_system_resource, future additions.\n return {\n kind: 'error',\n failure: { message: `model stopped: ${reason}`, code: reason.toUpperCase() },\n }\n }\n}\n\n/**\n * Map wire usage fields to the harness convention of DISJOINT counts. Cache\n * hits and reasoning tokens are carried only when the wire reported them.\n * @param usage - wire usage from the finish chunk or the trailing usage-only chunk.\n * @returns disjoint harness counts; cache/reasoning fields present only when the wire reported them.\n */\nexport function mapUsage(usage: WireUsage): TokenUsage {\n const cacheRead = usage.prompt_tokens_details?.cached_tokens\n const reasoning = usage.completion_tokens_details?.reasoning_tokens\n return {\n inputTokens: usage.prompt_tokens - (cacheRead ?? 0),\n outputTokens: usage.completion_tokens,\n ...cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {},\n ...reasoning !== undefined ? { reasoningTokens: reasoning } : {},\n }\n}\n\n/** Assemble the final ContentBlock for one open block. */\nfunction closeBlock(block: OpenBlock): ContentBlock {\n switch (block.kind) {\n case 'text': return { type: 'text', text: block.text }\n case 'reasoning': return { type: 'reasoning', text: block.text }\n case 'tool-call': return {\n type: 'tool-call',\n id: ToolCallId(block.callId ?? ''),\n name: block.name ?? '',\n arguments: block.text,\n }\n }\n}\n\n/**\n * Consume SSE data payloads (ending with `[DONE]`) and yield StreamChunks.\n * Malformed JSON payloads abort the stream with `MALFORMED_RESPONSE`.\n * @param payloads - SSE data payloads from {@link parseSse}, `[DONE]`-terminated.\n * @returns deltas as they arrive; `block-end`s, `usage`, and `finish` are all deferred to the `[DONE]` sentinel.\n * A `stop` (or absent) finish with no opened blocks is a degenerate provider completion and maps to an\n * `EMPTY_RESPONSE` error finish instead of a successful empty message.\n */\nexport async function* translate(payloads: AsyncIterable<string>): AsyncGenerator<StreamChunk> {\n let nextIndex = 0\n let textBlock: OpenBlock | undefined\n let reasoningBlock: OpenBlock | undefined\n const toolBlocks = new Map<number, OpenBlock>()\n const order: OpenBlock[] = []\n let pendingFinish: FinishReason | undefined\n let pendingUsage: TokenUsage | undefined\n\n function open(kind: OpenBlock['kind']): OpenBlock {\n const block: OpenBlock = { index: nextIndex++, kind, text: '' }\n order.push(block)\n return block\n }\n\n for await (const payload of payloads) {\n if (payload === DONE) {\n for (const block of order) {\n yield { type: 'block-end', index: block.index, block: closeBlock(block) }\n }\n if (pendingUsage) yield { type: 'usage', usage: pendingUsage }\n const reason = pendingFinish ?? { kind: 'stop' as const }\n yield {\n type: 'finish',\n reason: reason.kind === 'stop' && order.length === 0\n ? {\n kind: 'error',\n failure: { message: 'model returned a completed response with no content', code: EMPTY_RESPONSE_CODE },\n }\n : reason,\n }\n return\n }\n\n let chunk: WireChunk\n try {\n chunk = JSON.parse(payload) as WireChunk\n } catch {\n throw new LlmError(`malformed SSE payload: ${payload.slice(0, 120)}`, 'MALFORMED_RESPONSE')\n }\n\n for (const choice of chunk.choices ?? []) {\n const delta = choice.delta\n\n // Reasoning first: thinking mode interleaves it before text. The\n // empty-string first chunk must not open a block.\n const reasoning = delta?.reasoning\n if (typeof reasoning === 'string' && reasoning.length > 0) {\n if (!reasoningBlock) {\n reasoningBlock = open('reasoning')\n yield { type: 'block-start', index: reasoningBlock.index, blockType: 'reasoning' }\n }\n reasoningBlock.text += reasoning\n yield { type: 'reasoning-delta', index: reasoningBlock.index, text: reasoning }\n }\n\n const content = delta?.content\n if (typeof content === 'string' && content.length > 0) {\n if (!textBlock) {\n textBlock = open('text')\n yield { type: 'block-start', index: textBlock.index, blockType: 'text' }\n }\n textBlock.text += content\n yield { type: 'text-delta', index: textBlock.index, text: content }\n }\n\n for (const call of delta?.tool_calls ?? []) {\n let block = toolBlocks.get(call.index)\n if (!block) {\n block = open('tool-call')\n toolBlocks.set(call.index, block)\n yield { type: 'block-start', index: block.index, blockType: 'tool-call' }\n }\n if (call.id !== undefined) block.callId = call.id\n if (call.function?.name !== undefined) block.name = call.function.name\n const fragment = call.function?.arguments ?? ''\n block.text += fragment\n yield {\n type: 'tool-call-delta',\n index: block.index,\n id: ToolCallId(block.callId ?? ''),\n ...block.name !== undefined ? { name: block.name } : {},\n argumentsDelta: fragment,\n }\n }\n\n if (typeof choice.finish_reason === 'string') {\n pendingFinish = mapFinishReason(choice.finish_reason)\n }\n }\n\n // Usage may arrive attached to the finish chunk or as a trailing\n // usage-only chunk \u2014 keep the latest.\n if (chunk.usage) pendingUsage = mapUsage(chunk.usage)\n }\n\n // parseSse guarantees the [DONE] sentinel (or throws); reaching here means\n // the payload source violated that contract.\n throw new LlmError('SSE payload stream ended without [DONE]', 'STREAM_CLOSED')\n}\n", "/**\n * Answering \"which models can this draft serve?\" for the Models settings\n * page's fetch action.\n *\n * A draft naming this plugin's route is answered **from the adapter's own\n * catalog**, with no network call: the resolved section is the authoritative\n * list for the route, and it carries the cloud-suffixed ids requests actually\n * use. Only a draft carrying an endpoint \u2014 a gateway or OpenAI-compatible\n * mirror the catalog says nothing about \u2014 is interrogated over the wire at\n * `GET {baseURL}/models`, the one listing shape such endpoints agree on.\n *\n * Nothing here is stored: the request carries a draft the user is still\n * editing, and the reply is candidate metadata the surface offers for\n * adoption. The section remains the only thing that decides what the route\n * serves.\n *\n * @module llm-ollama-cloud/discovery\n */\n\nimport { INVALID_CREDENTIAL_CODE, LlmError, normalizeApiKey } from '@deepseek-ai/dsh-llm'\nimport type { LlmDiscoveredModel, LlmModelDiscoveryOperation } from '@deepseek-ai/dsh-llm'\nimport { attributionHeaders } from '@deepseek-ai/dsh-llm'\nimport type { OllamaCatalogModel } from './adapter.ts'\n\n/**\n * Endpoint replies larger than this are refused. The endpoint is whatever URL\n * the user typed, so the ceiling holds on the bytes actually read rather than\n * on the length the server claims.\n */\nconst MAX_RESPONSE_BYTES = 4 * 1024 * 1024\n\n/** One entry of an OpenAI-compatible `GET /models` reply. */\ninterface ListingEntry {\n id?: unknown\n /** Common gateway extensions; absent from the official listing. */\n name?: unknown\n display_name?: unknown\n context_window?: unknown\n context_length?: unknown\n max_tokens?: unknown\n max_output_tokens?: unknown\n}\n\n/** A positive integer field of a listing entry, or `undefined` when absent or unusable. */\nfunction capacity(...candidates: readonly unknown[]): number | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'number' && Number.isInteger(candidate) && candidate > 0) return candidate\n }\n return undefined\n}\n\n/** A non-empty string field of a listing entry, or `undefined`. */\nfunction label(...candidates: readonly unknown[]): string | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.length > 0) return candidate\n }\n return undefined\n}\n\n/**\n * Join the endpoint base with the listing path. The base is treated as a\n * prefix rather than a URL to resolve against, so a deployment path such as\n * `https://gateway.example/openai/v1` keeps its segments instead of losing\n * them to `URL` resolution.\n */\nfunction listingUrl(baseURL: string): string {\n return `${baseURL.replace(/\\/+$/, '')}/models`\n}\n\n/**\n * Accept one probe key, or refuse it before the header is built. Without this\n * the `fetch` below would throw a ByteString `TypeError` that the transport\n * catch reports as `could not reach <url>` \u2014 blaming the network for a local,\n * deterministic fault.\n * @param raw - the key typed into the form or read from storage.\n * @returns the trimmed, usable key.\n */\nfunction usableProbeKey(raw: string): string {\n const checked = normalizeApiKey(raw)\n if (checked.ok) return checked.value\n throw new LlmError(\n checked.reason === 'empty'\n ? 'this provider\\'s API key is blank; enter it on the Models page, or clear it to probe unauthenticated'\n : 'this provider\\'s API key contains characters no HTTP header can carry; paste the raw key only',\n INVALID_CREDENTIAL_CODE,\n )\n}\n\n/**\n * Read a reply body, refusing one that outgrows the ceiling. A declared length\n * is checked first so an honest server is turned away without transferring\n * anything; the accumulated total is what actually enforces the bound, because\n * a server that under-declares (or streams) tells us nothing up front.\n */\nasync function readBounded(response: Response, url: string): Promise<string> {\n const oversized = (): LlmError =>\n new LlmError(`${url} answered with more than ${MAX_RESPONSE_BYTES} bytes`, 'DISCOVERY_FAILED')\n const declared = Number(response.headers.get('content-length') ?? Number.NaN)\n if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {\n await response.body?.cancel()\n throw oversized()\n }\n if (response.body === null) return ''\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n total += value.byteLength\n if (total > MAX_RESPONSE_BYTES) throw oversized()\n chunks.push(value)\n }\n } finally {\n /* v8 ignore next 4 -- cancel() after a completed or abandoned read settles without rejecting; unobserved best-effort cleanup. */\n await reader.cancel().catch(() => {\n // Cancel after a drained read, or after this function walked away from\n // an oversized one, is cleanup; the reply is already decided either way.\n })\n }\n const body = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n body.set(chunk, offset)\n offset += chunk.byteLength\n }\n return new TextDecoder().decode(body)\n}\n\n/**\n * Read one OpenAI-compatible listing reply. Entries without a usable id are\n * skipped rather than failing the whole interrogation: a single malformed row\n * should not deny the user the rest of a working endpoint's catalog.\n */\nfunction readListing(body: unknown): LlmDiscoveredModel[] {\n const data = (body as { data?: unknown } | null)?.data\n if (!Array.isArray(data)) {\n throw new LlmError(\n 'the endpoint\\'s model listing has no \"data\" array; enter this provider\\'s models by hand',\n 'DISCOVERY_FAILED',\n )\n }\n const models: LlmDiscoveredModel[] = []\n for (const raw of data) {\n const entry = raw as ListingEntry | null\n const id = label(entry?.id)\n if (id === undefined) continue\n const name = label(entry?.name, entry?.display_name)\n const contextWindow = capacity(entry?.context_window, entry?.context_length)\n const maxTokens = capacity(entry?.max_output_tokens, entry?.max_tokens)\n models.push({\n id,\n ...name === undefined ? {} : { name },\n ...contextWindow === undefined ? {} : { contextWindow },\n ...maxTokens === undefined ? {} : { maxTokens },\n })\n }\n return models\n}\n\n/**\n * Interrogate one draft provider for the models it advertises.\n * @param request - the endpoint and one-shot credential to use.\n * @param installed - the route's own catalog as currently resolved; the\n * answer for a draft naming the route.\n * @param storedApiKey - the credential the stored section resolves, asked for\n * only when the draft carries none and only on the path that reaches the\n * network. A configuration surface never holds a stored secret \u2014 it edits a\n * redacted descriptor \u2014 so without this an already-configured route would be\n * interrogated unauthenticated and answer 401.\n * @returns the advertised models in endpoint order.\n * @throws LlmError when the draft names neither a catalog route nor an\n * endpoint, the endpoint refuses or fails the request, or the reply is not\n * a model listing.\n */\nexport async function discoverModels(\n request: LlmModelDiscoveryOperation,\n installed: readonly OllamaCatalogModel[],\n storedApiKey?: () => Promise<string | undefined>,\n): Promise<readonly LlmDiscoveredModel[]> {\n // A named route already has its answer, and a better one: the installed\n // entries carry the cloud-suffixed ids and capacities no listing endpoint\n // reports, and they are already normalized through the same step the\n // adapter's own requests go through.\n if (request.provider !== undefined && installed.length > 0) {\n return installed.map(model => ({\n id: model.id,\n name: model.name ?? model.id,\n ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },\n ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },\n }))\n }\n if (request.baseURL === undefined || request.baseURL.length === 0) {\n throw new LlmError(\n 'model discovery needs a baseURL to interrogate; set one, or enter this provider\\'s models by hand',\n 'DISCOVERY_FAILED',\n )\n }\n const url = listingUrl(request.baseURL)\n // A key typed into the form wins: it is the one the user is testing, and it\n // may be the replacement for exactly the stored key that is failing. The\n // stored one is only asked for here, past the catalog short-circuit, so a\n // route answered from the registry costs no credential lookup \u2014 and no\n // diagnostic about a credential it never needed. A probe carrying no key\n // stays unauthenticated, which is how an auth-free gateway is meant to be\n // asked.\n const supplied = request.apiKey ?? await storedApiKey?.()\n const apiKey = supplied === undefined ? undefined : usableProbeKey(supplied)\n let response: Response\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n ...apiKey === undefined ? {} : { authorization: `Bearer ${apiKey}` },\n ...attributionHeaders(),\n },\n ...request.signal === undefined ? {} : { signal: request.signal },\n })\n } catch (error: unknown) {\n if (request.signal?.aborted) {\n throw new LlmError('model discovery aborted by caller', 'ABORTED', { cause: error })\n }\n throw new LlmError(`could not reach ${url}`, 'DISCOVERY_FAILED', { cause: error })\n }\n if (!response.ok) {\n throw new LlmError(\n `${url} answered ${response.status}${response.status === 401 || response.status === 403 ? '; check the API key' : ''}`,\n 'DISCOVERY_FAILED',\n )\n }\n let text: string\n try {\n text = await readBounded(response, url)\n } catch (error: unknown) {\n // Cancellation during the body read rejects with the abort reason, which\n // may be any value; the caller gets the same coded failure it would have\n // for a cancellation before the request went out.\n if (request.signal?.aborted) {\n throw new LlmError('model discovery aborted by caller', 'ABORTED', { cause: error })\n }\n throw error\n }\n let body: unknown\n try {\n body = JSON.parse(text)\n } catch (error: unknown) {\n throw new LlmError(`${url} did not answer with JSON`, 'DISCOVERY_FAILED', { cause: error })\n }\n return readListing(body)\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * Register an {@link OllamaAdapter} for the `ollama-cloud-direct` provider\n * route on `ctx.llm`, with connection facts resolved per request instead of\n * frozen at load: the plugin layers its `cordis.yml` entry config under the\n * optional `llm-ollama-cloud` user-settings section (`ctx.settings`) and\n * resolves the bearer token through the credential seam (`ctx.credentials`),\n * falling back to the process environment, so a changed base URL, catalog, or\n * key reaches the very next request without restarting anything, while an\n * in-flight stream keeps the facts it started with. The one\n * registration-captured fact \u2014 the retry policy \u2014 re-registers the route in\n * place when it changes.\n *\n * The route is configured pi-ai-style, as a per-route profile under\n * `providers.ollama-cloud-direct`: with no stored profile the route is\n * **dormant on configuration surfaces** \u2014 declared in the configurable-provider\n * directory so the Models settings page lists it in the add-provider select \u2014\n * while the adapter itself serves the resolved defaults (schema defaults plus\n * this module's fallbacks) the moment the plugin mounts, so an ambient\n * `OLLAMA_CLOUD_API_KEY` keeps working before the page ever writes a profile.\n * A model-discovery registration answers the page's fetch action from the\n * resolved catalog, or interrogates a drafted endpoint.\n *\n * Dependencies are intentionally minimal \u2014 `@deepseek-ai/dsh-llm` (the harness\n * LLM seam contract), `@deepseek-ai/dsh-credentials` (the credential seam),\n * `@deepseek-ai/dsh-settings` (the settings-section install), `@deepseek-ai/\n * schemastery` (the section schema), `@deepseek-ai/cordis` (plugin framework),\n * and `eventsource-parser` (SSE framing); validation beyond the schema is\n * hand-rolled. The route is `ollama-cloud-direct` (not `ollama-cloud`) so it\n * can coexist with a pi-ai-configured `ollama-cloud` route.\n *\n * @module llm-ollama-cloud\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport z from '@deepseek-ai/schemastery'\nimport { assertUsableApiKey, LlmError, resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'\nimport type { ModelModality, RetryPolicyConfig } from '@deepseek-ai/dsh-llm'\nimport { credentialRef } from '@deepseek-ai/dsh-credentials'\nimport { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'\nimport {\n DEFAULT_CONTEXT_WINDOW,\n DEFAULT_MAX_TOKENS,\n DEFAULT_STREAM_IDLE_TIMEOUT_MS,\n MAX_TIMER_DELAY_MS,\n normalizeCloud,\n OllamaAdapter,\n} from './adapter.ts'\nimport type { OllamaCatalogModel, OllamaConnectionOptions } from './adapter.ts'\nimport { discoverModels } from './discovery.ts'\n\nexport {\n DEFAULT_CONTEXT_WINDOW,\n DEFAULT_MAX_TOKENS,\n DEFAULT_STREAM_IDLE_TIMEOUT_MS,\n MAX_TIMER_DELAY_MS,\n normalizeCloud,\n OllamaAdapter,\n} from './adapter.ts'\nexport type { OllamaAdapterOptions, OllamaCatalogModel, OllamaConnectionOptions } from './adapter.ts'\nexport { discoverModels } from './discovery.ts'\nexport type { RequestDefaults } from './serialize.ts'\nexport type * from './types.ts'\n\nexport const name = 'llm-ollama-cloud'\nexport const inject = ['llm']\n\nconst NS = settingsNamespace('llm-ollama-cloud')\nconst DEFAULT_API_KEY_ENV = 'OLLAMA_CLOUD_API_KEY'\n/** The single provider route this plugin owns. */\nexport const PROVIDER = 'ollama-cloud-direct'\n\nconst DEFAULT_MODELS: OllamaCatalogModel[] = [\n { id: 'deepseek-v4-flash:cloud', name: 'DeepSeek-V4-Flash (cloud)', contextWindow: DEFAULT_CONTEXT_WINDOW },\n { id: 'deepseek-v4-pro:cloud', name: 'DeepSeek-V4-Pro (cloud)', contextWindow: DEFAULT_CONTEXT_WINDOW },\n { id: 'glm-5.2:cloud', name: 'GLM-5.2 (cloud)', contextWindow: DEFAULT_CONTEXT_WINDOW },\n]\n\nconst MODEL_MODALITIES = ['text', 'image'] as const satisfies readonly ModelModality[]\n\n/**\n * One route's stored profile \u2014 the plugin config's per-route entry and the\n * shape the Models page writes under `providers.<route>`. Every field is\n * optional: a profile naming no reference resolves key material through\n * {@link OllamaProviderProfile.apiKeyEnv}'s default at each request, omitted\n * thinking mode uses the provider default, and omitted reasoning effort lets\n * the server auto-enable thinking at its default.\n */\nexport interface OllamaProviderProfile {\n /** Credential reference (environment-variable name) resolved per request; defaults to `OLLAMA_CLOUD_API_KEY`. */\n apiKeyEnv?: string\n /** Endpoint base; defaults to the Ollama cloud API. */\n baseURL?: string\n /** Deployment thinking policy; `disabled` limits every conversation request to `none` effort. */\n thinking?: 'enabled' | 'disabled'\n /** Default thinking effort (default unset, so the server picks); `off` maps to wire `none`. */\n reasoningEffort?: 'off' | 'low' | 'high' | 'max'\n /** Default per-request output cap (default 65,536); a model's own cap and explicit request values win. */\n maxTokens?: number\n /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */\n defaultContextWindow?: number\n /** Advisory models shown by discovery consumers; a missing `:cloud` suffix is appended. */\n models?: OllamaCatalogModel[]\n /** Maximum provider idle time while one stream read is outstanding (default five minutes). */\n streamIdleTimeoutMs?: number\n /** Provider-owned model-request retry policy; omission uses normal mode with five retries. */\n retryPolicy?: RetryPolicyConfig\n}\n\n/**\n * Plugin config from the `cordis.yml` mount entry, validated by the\n * same-named schemastery schema and doubling as the `llm-ollama-cloud`\n * settings-section shape. Profiles are keyed by provider route id; the route\n * this plugin serves is {@link PROVIDER}. A mount that pins the profile\n * presents the route as configured; a bare mount leaves it dormant in the\n * add-provider select until the page (or `settings.yaml`) writes one.\n */\nexport interface Config {\n /** Per-route profiles keyed by provider route id. */\n providers?: Record<string, OllamaProviderProfile>\n}\n\n/** The catalog-model entry schema (one profile's `models` row). */\nconst catalogModel: z<OllamaCatalogModel> = z.object({\n id: z.string().required(),\n name: z.string(),\n description: z.string(),\n contextWindow: z.number().step(1).min(1),\n maxTokens: z.number().step(1).min(1),\n inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(['text']),\n})\n\n/** One stored route profile; its defaults apply only once the profile exists. */\nconst profileSchema: z<OllamaProviderProfile> = z.object({\n apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),\n baseURL: z.string(),\n thinking: z.union(['enabled', 'disabled']),\n reasoningEffort: z.union(['off', 'low', 'high', 'max']),\n maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS),\n defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),\n models: z.array(catalogModel).default(DEFAULT_MODELS),\n streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),\n retryPolicy: RetryPolicySchema,\n})\n\n/** The `llm-ollama-cloud` settings-section schema; `Config` is its static type. */\nexport const Config: z<Config> = z.object({\n providers: z.dict(profileSchema).default({}),\n})\n\n/** The public Ollama cloud API base. */\nexport const PUBLIC_BASE_URL = 'https://ollama.com/v1'\n\n/** Resolve, validate, and detach the advisory model catalog, normalizing every id to cloud naming. */\nfunction resolveModels(models: readonly OllamaCatalogModel[] | undefined): OllamaCatalogModel[] {\n const seen = new Set<string>()\n return (models ?? DEFAULT_MODELS).map((model) => {\n if (model.id.length === 0) throw new Error('llm-ollama-cloud: catalog model ids must be non-empty')\n const id = normalizeCloud(model.id)\n if (model.name !== undefined && model.name.length === 0) {\n throw new Error(`llm-ollama-cloud: catalog model \"${id}\" has an empty name`)\n }\n if (model.contextWindow !== undefined\n && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) {\n throw new Error(\n `llm-ollama-cloud: catalog model \"${id}\" contextWindow must be a positive integer`,\n )\n }\n if (model.maxTokens !== undefined\n && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {\n throw new Error(\n `llm-ollama-cloud: catalog model \"${id}\" maxTokens must be a positive integer`,\n )\n }\n const inputModalities = model.inputModalities ?? ['text']\n if (inputModalities.length === 0) {\n throw new Error(`llm-ollama-cloud: catalog model \"${id}\" inputModalities must not be empty`)\n }\n if (inputModalities.some(modality => !MODEL_MODALITIES.includes(modality))) {\n throw new Error(\n `llm-ollama-cloud: catalog model \"${id}\" inputModalities must contain only \"text\" and \"image\"`,\n )\n }\n if (new Set(inputModalities).size !== inputModalities.length) {\n throw new Error(`llm-ollama-cloud: catalog model \"${id}\" inputModalities must not contain duplicates`)\n }\n if (seen.has(id)) throw new Error(`llm-ollama-cloud: duplicate catalog model \"${id}\"`)\n seen.add(id)\n return {\n id,\n ...model.name === undefined ? {} : { name: model.name },\n ...model.description === undefined ? {} : { description: model.description },\n ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },\n ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },\n inputModalities: [...inputModalities],\n }\n })\n}\n\n/**\n * The one explicit resolve step from raw config to validated connection\n * facts, with every default and bound re-judged here (fail loud at load).\n * Programmatic construction may bypass Schemastery normalization, so this\n * also re-judges each settings snapshot at its first use.\n * @param config - raw plugin config or resolved settings snapshot.\n * @returns validated connection facts for {@link PROVIDER}.\n */\nexport function resolveAdapterOptions(config: Config): OllamaConnectionOptions {\n return resolveProfileOptions(config.providers?.[PROVIDER])\n}\n\n/**\n * Resolve one raw profile into validated connection facts. A missing profile\n * resolves the defaults, which is the dormant route's serving posture.\n * @param profile - raw profile fields, or `undefined` when none is stored.\n * @returns validated connection facts plus the credential reference.\n */\nexport function resolveProfileOptions(profile: OllamaProviderProfile | undefined): OllamaConnectionOptions {\n if (profile?.thinking === 'disabled'\n && profile.reasoningEffort !== undefined\n && profile.reasoningEffort !== 'off') {\n throw new Error('llm-ollama-cloud: only reasoningEffort \"off\" can be configured when thinking is disabled')\n }\n if (profile?.defaultContextWindow !== undefined\n && (!Number.isInteger(profile.defaultContextWindow) || profile.defaultContextWindow <= 0)) {\n throw new Error('llm-ollama-cloud: defaultContextWindow must be a positive integer')\n }\n if (profile?.maxTokens !== undefined\n && (!Number.isSafeInteger(profile.maxTokens) || profile.maxTokens <= 0)) {\n throw new Error('llm-ollama-cloud: maxTokens must be a positive safe integer')\n }\n const streamIdleTimeoutMs = profile?.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS\n if (!Number.isFinite(streamIdleTimeoutMs)\n || streamIdleTimeoutMs <= 0\n || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) {\n throw new Error(\n `llm-ollama-cloud: streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`,\n )\n }\n return {\n apiKeyEnv: credentialRef(profile?.apiKeyEnv ?? DEFAULT_API_KEY_ENV),\n baseURL: profile?.baseURL ?? PUBLIC_BASE_URL,\n defaults: {\n thinking: profile?.thinking,\n reasoningEffort: profile?.reasoningEffort,\n },\n maxTokens: profile?.maxTokens ?? DEFAULT_MAX_TOKENS,\n defaultContextWindow: profile?.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,\n models: resolveModels(profile?.models),\n streamIdleTimeoutMs,\n retryPolicy: resolveRetryPolicy(profile?.retryPolicy, 'llm-ollama-cloud: retryPolicy'),\n }\n}\n\n/** The `ctx.credentials` service surface this plugin uses (dsh-credentials). */\ninterface CredentialsLike {\n resolve(ref: string): Promise<{ value: string; source: string } | undefined>\n}\n\nexport function apply(ctx: Context, config: Config = {}): void {\n let current: () => Config = () => config\n let lastRaw: Config | undefined\n let lastGood: OllamaConnectionOptions | undefined\n const options = (): OllamaConnectionOptions => {\n const raw = current()\n if (raw === lastRaw && lastGood !== undefined) return lastGood\n try {\n const next = resolveAdapterOptions(raw)\n lastRaw = raw\n lastGood = next\n return next\n } catch (error) {\n // Static composition resolves before anything registers, so this branch\n // only sees a live settings snapshot failing a beyond-schema bound:\n // keep serving the last good facts and say so once per bad snapshot.\n if (lastGood === undefined) throw error\n lastRaw = raw\n ctx.logger.error('llm-ollama-cloud: keeping the last good configuration after an invalid settings section')\n ctx.logger.error(error)\n return lastGood\n }\n }\n options()\n\n const resolveApiKey = async (connection: OllamaConnectionOptions): Promise<string> => {\n // Every credential fact comes from the caller's snapshot, so a rejected\n // settings generation cannot leak its key onto the previous endpoint.\n const ref = connection.apiKeyEnv\n const credentials = ctx.get('credentials') as CredentialsLike | undefined\n if (credentials !== undefined) {\n const hit = await credentials.resolve(ref)\n if (hit !== undefined && hit.value.length > 0) {\n return assertUsableApiKey(hit.value, 'llm-ollama-cloud', ref)\n }\n }\n const ambient = process.env[ref]\n if (ambient !== undefined && ambient.length > 0) {\n return assertUsableApiKey(ambient, 'llm-ollama-cloud', ref)\n }\n throw new LlmError(\n `llm-ollama-cloud: no API key for provider route \"${PROVIDER}\"; store ${ref} through the credentials`\n + ` service (the web Models page writes it), or export ${ref} in the launching environment`,\n 'MISSING_CREDENTIAL',\n )\n }\n /**\n * The stored credential, for a probe whose draft carries none. Missing is\n * an answer here (`undefined`, probe unauthenticated), not a failure \u2014 the\n * request path owns the loud MISSING_CREDENTIAL refusal.\n */\n const storedApiKey = async (): Promise<string | undefined> => {\n const ref = options().apiKeyEnv\n const credentials = ctx.get('credentials') as CredentialsLike | undefined\n const hit = credentials !== undefined ? (await credentials.resolve(ref))?.value : undefined\n const value = hit !== undefined && hit.length > 0 ? hit : process.env[ref]\n return value !== undefined && value.length > 0 ? value : undefined\n }\n\n const adapter = new OllamaAdapter({ options, resolveApiKey })\n // Declared even while dormant, so configuration surfaces list the route in\n // the add-provider select before any profile exists.\n ctx.llm.registerConfigurableProviders([\n { provider: PROVIDER, displayName: 'ollama-cloud', settingsNs: NS, settingsPath: ['providers', PROVIDER] },\n ])\n // Route effects bind to this apply fiber via the stable `ctx` reference,\n // even when a swap runs inside the scoped settings callback below.\n const registration = ctx.llm.registerAdapter([PROVIDER], adapter)\n let registeredPolicy = options().retryPolicy\n const ensureRegistrationFacts = (): void => {\n const policy = options().retryPolicy\n if (deepEqualJson(policy, registeredPolicy)) return\n // The registry captures the retry policy at registration, so it is the one\n // fact per-request resolution cannot refresh. `replace` re-reads it in one\n // synchronous registry section: disposing and re-registering instead would\n // publish an empty route set between the two, and an observer that reacted\n // to it would see this provider disappear and come back.\n registration.replace([PROVIDER])\n registeredPolicy = policy\n }\n // The Models page's fetch action: a draft naming this route answers from the\n // resolved catalog; anything else is interrogated at the endpoint it shows.\n ctx.llm.registerModelDiscovery(NS, (request, signal) => discoverModels(\n { ...request, ...signal === undefined ? {} : { signal } },\n options().models,\n storedApiKey,\n ))\n installSettingsSection(ctx, NS, Config, config, {\n setSource: (source) => {\n current = source\n },\n onChange: ensureRegistrationFacts,\n })\n}\n", "/**\n * `OllamaAdapter`: fetch + SSE against an Ollama (OpenAI-compatible)\n * chat-completions endpoint, emitting harness StreamChunks. Transport-only:\n * connection facts arrive through a thunk resolved once per operation and the\n * bearer token through a per-request resolver.\n *\n * Model ids are normalized to Ollama's `:cloud` naming on every operation: a\n * request for `deepseek-v4-flash` is sent as `deepseek-v4-flash:cloud`, and an\n * already-suffixed id is forwarded unchanged.\n *\n * Dependencies are intentionally minimal: `@deepseek-ai/dsh-llm` (the harness\n * LLM seam contract), `@deepseek-ai/cordis` (plugin framework), and\n * `eventsource-parser` (SSE framing). Everything else is hand-rolled here.\n *\n * @module llm-ollama-cloud/adapter\n */\n\nimport {\n attributionHeaders,\n contentHasImage,\n CONTEXT_WINDOW_EXCEEDED_CODE,\n isContextWindowExceededError,\n isQuotaExceededError,\n LlmAdapter,\n LlmError,\n ProviderRequestId,\n QUOTA_EXCEEDED_CODE,\n ReasoningEffortId,\n} from '@deepseek-ai/dsh-llm'\nimport type {\n GenerateOptions,\n LlmModelInfo,\n LlmProviderInfo,\n LlmResolvedModelInfo,\n ModelModality,\n ResolvedRetryPolicy,\n StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { serializeRequest } from './serialize.ts'\nimport type { RequestDefaults } from './serialize.ts'\nimport { parseSse } from './sse.ts'\nimport { translate } from './translate.ts'\nimport type { WireError } from './types.ts'\n\n/** One optional model entry advertised by the direct-fetch adapter. */\nexport interface OllamaCatalogModel {\n /** Wire model id accepted by the configured endpoint; a missing `:cloud` suffix is appended. */\n id: string\n /** Selector label; defaults to {@link id}. */\n name?: string\n /** Optional selector detail for deployments with similar model variants. */\n description?: string\n /** Known combined request/response context capacity; omitted when deployment metadata is unavailable. */\n contextWindow?: number\n /** Per-request output cap for this model; omission falls back to the profile's {@link OllamaConnectionOptions.maxTokens}. */\n maxTokens?: number\n /** Accepted request modalities; omission is text-only. */\n inputModalities?: ModelModality[]\n}\n\n/**\n * Validated connection facts for one operation. The plugin's\n * `resolveAdapterOptions` is the one explicit resolve step producing this\n * shape; the adapter trusts it and re-reads it per operation.\n */\nexport interface OllamaConnectionOptions {\n /** Endpoint base; `/chat/completions` is appended. */\n baseURL: string\n /** Environment-variable name holding the bearer token, resolved per request. */\n apiKeyEnv: string\n /** Request defaults applied to every call (thinking mode, effort). */\n defaults: RequestDefaults\n /** Default per-request output cap; explicit request values win. */\n maxTokens: number\n /** Positive context capacity used when the selected model has no exact value. */\n defaultContextWindow: number\n /** Advisory models exposed to discovery consumers; requests remain unrestricted. */\n models: readonly OllamaCatalogModel[]\n /** Maximum provider idle time while one stream read is outstanding. */\n streamIdleTimeoutMs: number\n /** Provider-owned model-request retry policy, already resolved. */\n retryPolicy: ResolvedRetryPolicy\n}\n\n/** Constructor options for {@link OllamaAdapter}: the operation-local resolution hooks the plugin owns. */\nexport interface OllamaAdapterOptions {\n /** Current validated connection facts; called once per operation. */\n options: () => OllamaConnectionOptions\n /**\n * Resolve the bearer token for the connection facts of one request. The\n * snapshot is passed in \u2014 never re-read \u2014 so the key can only ever come\n * from the same resolution as the endpoint it is sent to.\n */\n resolveApiKey: (connection: OllamaConnectionOptions) => Promise<string>\n}\n\n/** Default maximum idle interval while an adapter stream read is outstanding. */\nexport const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000\n/** Default combined request/response context capacity. */\nexport const DEFAULT_CONTEXT_WINDOW = 1_000_000\n/** Default per-request output-token cap. */\nexport const DEFAULT_MAX_TOKENS = 65_536\n/** The Ollama cloud model-name suffix this adapter appends when missing. */\nexport const CLOUD_SUFFIX = ':cloud'\n/** Largest value `setTimeout` accepts (2^31 - 1 ms). */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647\nconst STREAM_IDLE_TIMEOUT_CODE = 'LLM_STREAM_IDLE_TIMEOUT'\nconst OFF_REASONING_EFFORT = ReasoningEffortId('off')\nconst LOW_REASONING_EFFORT = ReasoningEffortId('low')\nconst HIGH_REASONING_EFFORT = ReasoningEffortId('high')\nconst MAX_REASONING_EFFORT = ReasoningEffortId('max')\nconst REASONING_EFFORTS = [\n { id: OFF_REASONING_EFFORT, name: 'Off' },\n { id: LOW_REASONING_EFFORT, name: 'Low' },\n { id: HIGH_REASONING_EFFORT, name: 'High' },\n { id: MAX_REASONING_EFFORT, name: 'Max' },\n] as const\nconst OFF_ONLY_REASONING_EFFORTS = [\n { id: OFF_REASONING_EFFORT, name: 'Off' },\n] as const\n\n/**\n * Normalize a model id to Ollama's cloud naming. An id already carrying the\n * `:cloud` suffix is returned unchanged; any other id gets it appended. This\n * is the one place a bare harness model name becomes a wire model name.\n * @param model - the requested model id.\n * @returns the id with a `:cloud` suffix.\n */\nexport function normalizeCloud(model: string): string {\n return model.endsWith(CLOUD_SUFFIX) ? model : `${model}${CLOUD_SUFFIX}`\n}\n\n/**\n * Minimal idle watchdog: arms a timer on construction and after every read,\n * and aborts its signal when the idle budget elapses without a pulse. The\n * {@link OllamaAdapter} maps the expired flag to `TIMEOUT` and the caller's\n * own abort to `ABORTED`.\n */\nclass IdleWatchdog {\n private readonly controller = new AbortController()\n private timer: ReturnType<typeof setTimeout> | undefined\n private expired = false\n /** Combined caller + watchdog signal; aborts when either fires. */\n readonly signal: AbortSignal\n\n constructor(upstream: AbortSignal, private readonly timeoutMs: number) {\n this.signal = upstream.aborted\n ? upstream\n : AbortSignal.any([upstream, this.controller.signal])\n if (!upstream.aborted) {\n upstream.addEventListener('abort', () => this.stop(), { once: true })\n }\n }\n\n get didExpire(): boolean {\n return this.expired\n }\n\n private arm(): void {\n this.stop()\n this.timer = setTimeout(() => {\n this.expired = true\n this.controller.abort(new Error(STREAM_IDLE_TIMEOUT_CODE))\n }, this.timeoutMs)\n }\n\n /** Rearm the idle window; called after each provider read. */\n pulse(): void {\n this.arm()\n }\n\n stop(): void {\n if (this.timer !== undefined) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n }\n}\n\nfunction modelInfo(provider: string, model: OllamaCatalogModel): LlmModelInfo {\n return {\n provider,\n id: model.id,\n name: model.name ?? model.id,\n ...model.description === undefined ? {} : { description: model.description },\n inputModalities: model.inputModalities ?? ['text'],\n }\n}\n\nfunction providerRetryAfterMs(value: string | null): number | undefined {\n if (value === null) return undefined\n if (/^\\d+$/.test(value)) {\n const delay = Number(value) * 1_000\n return Number.isFinite(delay) && delay > 0 ? delay : undefined\n }\n const delay = Date.parse(value) - Date.now()\n return Number.isFinite(delay) && delay > 0 ? delay : undefined\n}\n\nfunction requestId(headers: Headers): ReturnType<typeof ProviderRequestId> | undefined {\n const value = headers.get('x-request-id') ?? headers.get('x-ollama-request-id')\n return value === null || value.length === 0 ? undefined : ProviderRequestId(value)\n}\n\n/**\n * Map an HTTP status to a stable LlmError code.\n * @param status - status of a non-2xx provider response.\n * @param error - parsed provider error body, when available.\n * @returns the normalized harness error code.\n */\nexport function httpErrorCode(status: number, error?: WireError['error']): string {\n if (status === 401 || status === 403) return 'AUTH'\n if (status === 413) return 'INVALID_REQUEST'\n const detail = [error?.code, error?.type, error?.message].filter(Boolean).join(' ')\n if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE\n if (status === 429) return 'RATE_LIMIT'\n if (status === 400) {\n if (isContextWindowExceededError(detail)) return CONTEXT_WINDOW_EXCEEDED_CODE\n return 'INVALID_REQUEST'\n }\n if (status >= 500) return 'SERVER'\n return `HTTP_${status}`\n}\n\n/**\n * One instance serves every model name it was registered under. The harness\n * model name is normalized to its cloud form and IS the wire model name.\n *\n * One stable signal reaches both initial fetch and body reads. Caller aborts\n * map to `ABORTED`; the configured per-read idle watchdog maps to `TIMEOUT`.\n */\nexport class OllamaAdapter extends LlmAdapter {\n constructor(private readonly config: OllamaAdapterOptions) {\n super()\n }\n\n override providerInfo(provider: string): LlmProviderInfo {\n // Matches the configurable-provider directory's displayName, so the\n // Models page row and the model-picker group name read as one provider.\n return { id: provider, name: 'ollama-cloud' }\n }\n\n override providerRetryPolicy(_provider: string): ResolvedRetryPolicy {\n return this.config.options().retryPolicy\n }\n\n override listModels(provider: string): Promise<readonly LlmModelInfo[]> {\n return Promise.resolve(this.config.options().models.map(model => modelInfo(provider, model)))\n }\n\n override resolveModel(\n provider: string,\n model: string,\n _signal?: AbortSignal,\n ): Promise<LlmResolvedModelInfo> {\n const connection = this.config.options()\n // Resolve against the wire (cloud-suffixed) id so an unsuffixed request\n // still matches its catalog entry and reports the cloud id onward.\n const wireModel = normalizeCloud(model)\n const configured = connection.models.find(entry => entry.id === wireModel)\n const contextWindow = configured?.contextWindow\n ?? connection.defaultContextWindow\n return Promise.resolve({\n // An uncatalogued endpoint is safely treated as text-only.\n ...configured === undefined\n ? { provider, id: wireModel, name: wireModel, inputModalities: ['text' as const] }\n : modelInfo(provider, configured),\n context: { contextWindow },\n defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,\n ...connection.defaults.thinking === 'disabled'\n ? {\n reasoning: {\n efforts: OFF_ONLY_REASONING_EFFORTS,\n defaultEffort: OFF_REASONING_EFFORT,\n },\n }\n : {\n reasoning: {\n efforts: REASONING_EFFORTS,\n defaultEffort: connection.defaults.reasoningEffort === 'off'\n ? OFF_REASONING_EFFORT\n : connection.defaults.reasoningEffort === 'low'\n ? LOW_REASONING_EFFORT\n : connection.defaults.reasoningEffort === 'max'\n ? MAX_REASONING_EFFORT\n : HIGH_REASONING_EFFORT,\n },\n },\n })\n }\n\n async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {\n // One resolution per stream call: connection facts and the credential\n // freeze here and hold for this whole request.\n const connection = this.config.options()\n if (options.messages.some(message => contentHasImage(message.content))) {\n throw new LlmError(\n 'Ollama image input is not supported yet.',\n 'UNSUPPORTED_CONTENT',\n )\n }\n const apiKey = await this.config.resolveApiKey(connection)\n const consumer = new AbortController()\n const upstream = options.signal === undefined\n ? consumer.signal\n : AbortSignal.any([options.signal, consumer.signal])\n const watchdog = new IdleWatchdog(upstream, connection.streamIdleTimeoutMs)\n const iterator = this.request(\n options,\n watchdog.signal,\n connection,\n apiKey,\n () => watchdog.pulse(),\n )[Symbol.asyncIterator]()\n let exhausted = false\n try {\n while (true) {\n watchdog.pulse()\n const result = await iterator.next()\n if (result.done) {\n exhausted = true\n return\n }\n yield result.value\n }\n } catch (error: unknown) {\n if (watchdog.didExpire) {\n throw new LlmError(\n `Ollama stream idle timeout after ${connection.streamIdleTimeoutMs}ms`,\n 'TIMEOUT',\n { cause: error },\n )\n }\n if (options.signal?.aborted) {\n throw new LlmError('Ollama request aborted by caller', 'ABORTED', { cause: error })\n }\n if (error instanceof LlmError) throw error\n throw new LlmError(`Ollama API stream from ${connection.baseURL} failed`, 'TRANSPORT', { cause: error })\n } finally {\n watchdog.stop()\n consumer.abort('Ollama stream consumer stopped')\n if (!exhausted && iterator.return !== undefined) {\n try {\n await iterator.return()\n } catch (_abortedTransportTeardown) {\n // The consumer controller already owns termination; a return-time abort cannot add a second outcome.\n }\n }\n }\n }\n\n private async * request(\n options: GenerateOptions,\n signal: AbortSignal,\n connection: OllamaConnectionOptions,\n apiKey: string,\n onComment: () => void,\n ): AsyncIterable<StreamChunk> {\n const body = serializeRequest(\n { ...options, model: normalizeCloud(options.model) },\n connection.defaults,\n )\n // Prepared outside the try so the TRANSPORT label below covers exactly the\n // transport boundary, never a serialization failure.\n const payload = JSON.stringify(body)\n const headers = {\n 'authorization': `Bearer ${apiKey}`,\n 'content-type': 'application/json',\n 'accept': 'text/event-stream',\n ...attributionHeaders(),\n ...options.sessionId !== undefined\n ? { 'x-deepseek-harness-session-id': String(options.sessionId) }\n : {},\n ...options.purpose === 'compaction'\n ? { 'x-deepseek-harness-compact': '1' }\n : {},\n }\n\n let response: Response\n try {\n response = await fetch(`${connection.baseURL}/chat/completions`, {\n method: 'POST',\n headers,\n body: payload,\n signal,\n })\n } catch (error: unknown) {\n // The outer stream distinguishes caller cancellation and watchdog expiry.\n if (signal.aborted) throw error\n // fetch wraps every transport failure (DNS, refused connection, TLS,\n // proxy) in a bare `TypeError: fetch failed` whose actionable detail\n // lives on `cause`.\n throw new LlmError(\n `Ollama API request to ${connection.baseURL} failed`,\n 'TRANSPORT',\n { cause: error },\n )\n }\n\n if (!response.ok) {\n let message = `Ollama API error (HTTP ${response.status})`\n let providerError: WireError['error']\n try {\n const parsed = await response.json() as WireError\n providerError = parsed.error\n if (providerError?.message) message = providerError.message\n } catch {\n // Only swallow error-body parsing: the HTTP status still identifies the\n // failure, so malformed gateway JSON must not mask it.\n }\n const delay = providerRetryAfterMs(response.headers.get('retry-after'))\n const id = requestId(response.headers)\n throw new LlmError(message, httpErrorCode(response.status, providerError), {\n status: response.status,\n ...delay === undefined ? {} : { providerRetryAfterMs: delay },\n ...id === undefined ? {} : { requestId: id },\n })\n }\n if (!response.body) {\n throw new LlmError('Ollama API returned no response body', 'EMPTY_RESPONSE')\n }\n\n yield* translate(parseSse(response.body, onComment))\n }\n}\n", "/**\n * Serialize harness messages into an Ollama chat completions request.\n * Text-only (the OpenAI-compatible endpoint's image path is deferred); tool\n * results become standalone `role: 'tool'` messages. Reasoning is replayed as\n * the `reasoning` assistant field only for reasoning-capable models (a wire id\n * containing `deepseek`), so non-reasoning models keep clean traces.\n * @module dsh-llm-ollama-cloud/serialize\n */\n\nimport { contentHasImage, LlmError } from '@deepseek-ai/dsh-llm'\nimport type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'\nimport type {\n WireMessage,\n WireRequest,\n WireTool,\n} from './types.ts'\n\n/** Adapter-level request defaults (from plugin config). */\nexport interface RequestDefaults {\n thinking?: 'enabled' | 'disabled' | undefined\n reasoningEffort?: 'off' | 'low' | 'high' | 'max' | undefined\n}\n\n/** The Ollama reasoning-effort values this adapter emits on the wire. */\nexport type WireReasoningEffort = 'none' | 'low' | 'high' | 'max'\n\ninterface ResolvedThinking {\n reasoningEffort?: WireReasoningEffort\n}\n\n/**\n * Whether a model's reasoning should be passed back on assistant history.\n * Only reasoning-capable models accept the `reasoning` field; a non-reasoning\n * model ignores it, so it is written only for wire ids containing `deepseek`.\n * @param model - the wire model id.\n * @returns true when the model is treated as reasoning-capable.\n */\nexport function passReasoning(model: string): boolean {\n return model.includes('deepseek')\n}\n\n/** Validate the adapter-owned effort before resolving its Ollama wire value. */\nfunction reasoningEffort(effort: NonNullable<GenerateOptions['reasoningEffort']>): 'off' | 'low' | 'high' | 'max' {\n if (effort === 'off' || effort === 'low' || effort === 'high' || effort === 'max') {\n return effort as 'off' | 'low' | 'high' | 'max'\n }\n throw new LlmError(\n `Ollama does not support reasoning effort \"${effort}\"`,\n 'UNSUPPORTED_REASONING_EFFORT',\n )\n}\n\n/**\n * Resolve one legal thinking/effort pair into an Ollama wire effort. An `off`\n * (or a `disabled` deployment default) maps to `none`; an explicit effort maps\n * to its Ollama spelling; an omitted effort with thinking enabled sends\n * nothing so the server auto-enables thinking at its default.\n * @param options - the harness request.\n * @param defaults - adapter-level thinking defaults.\n * @returns the wire `reasoning_effort`, or nothing when the server default should apply.\n */\nfunction resolveThinking(options: GenerateOptions, defaults: RequestDefaults): ResolvedThinking {\n if (options.purpose === 'session-title') return { reasoningEffort: 'none' }\n const effort = options.reasoningEffort === undefined\n ? defaults.reasoningEffort\n : reasoningEffort(options.reasoningEffort)\n if (defaults.thinking === 'disabled' && effort !== undefined && effort !== 'off') {\n throw new LlmError(\n `Ollama deployment does not support reasoning effort \"${effort}\"`,\n 'UNSUPPORTED_REASONING_EFFORT',\n )\n }\n if (effort === 'off') return { reasoningEffort: 'none' }\n if (effort === 'low' || effort === 'high' || effort === 'max') {\n return { reasoningEffort: effort }\n }\n // effort undefined: disabled defaults suppress reasoning, enabled or unset\n // ones send nothing and let the server pick its default.\n return defaults.thinking === 'disabled' ? { reasoningEffort: 'none' } : {}\n}\n\n/** Join the text blocks of a message (used for user/tool-result content). */\nfunction flattenText(blocks: ContentBlock[]): string {\n return blocks\n .filter(block => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\n/** Reject core image content before any text-flattening path can silently erase it. */\nfunction assertTextOnly(blocks: readonly ContentBlock[]): void {\n if (contentHasImage(blocks)) {\n throw new LlmError('The Ollama chat-completions adapter does not support image content yet.', 'UNSUPPORTED_CONTENT')\n }\n}\n\n/** Serialize one assistant message (text + optional reasoning + tool calls). */\nfunction serializeAssistant(message: Message, model: string): WireMessage {\n const text = flattenText(message.content)\n const reasoning = message.content\n .filter(block => block.type === 'reasoning')\n .map(block => block.text)\n .join('')\n const toolCalls = message.content\n .filter(block => block.type === 'tool-call')\n .map(block => ({\n id: block.id,\n type: 'function' as const,\n function: { name: block.name, arguments: block.arguments },\n }))\n\n return {\n role: 'assistant',\n // Text-less turns send \"\" \u2014 NEVER null. Reasoning-only turns (the model\n // can answer entirely in the reasoning channel) risk a gateway 400, and\n // since the message sits durably in the session log, a null here bricks\n // every later turn of that session.\n content: text,\n // CoT passback only for reasoning-capable models, via the `reasoning`\n // field Ollama accepts on assistant history.\n ...passReasoning(model) && reasoning.length > 0 ? { reasoning } : {},\n ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},\n }\n}\n\n/**\n * Serialize the conversation. `tool-result` blocks become standalone\n * `{role: 'tool'}` messages; the harness puts each tool result in its own\n * user-role message, so a mixed user message contributes its text first and\n * its tool results as separate wire messages after.\n * @param model - the wire model id, used to decide reasoning passback.\n * @param messages - the harness conversation, in order.\n * @returns the wire messages; order preserved, each tool result expanded into its own entry.\n */\nexport function serializeMessages(model: string, messages: Message[]): WireMessage[] {\n const wire: WireMessage[] = []\n for (const message of messages) {\n assertTextOnly(message.content)\n if (message.role === 'system') {\n wire.push({ role: 'system', content: flattenText(message.content) })\n continue\n }\n if (message.role === 'assistant') {\n wire.push(serializeAssistant(message, model))\n continue\n }\n // user role: tool results ride in user messages in the harness\n // vocabulary, but Ollama wants them as role:'tool' messages.\n const toolResults = message.content.filter(block => block.type === 'tool-result')\n const text = flattenText(message.content)\n if (text.length > 0 || toolResults.length === 0) {\n wire.push({ role: 'user', content: text })\n }\n for (const result of toolResults) {\n wire.push({\n role: 'tool',\n tool_call_id: result.toolCallId,\n // Empty tool output still needs SOME content on the wire.\n content: flattenText(result.content) || '(no output)',\n })\n }\n }\n return wire\n}\n\n/** Assemble request fields shared by every conversion. */\nfunction requestWithMessages(\n options: GenerateOptions,\n messages: WireMessage[],\n defaults: RequestDefaults,\n): WireRequest {\n const tools: WireTool[] | undefined = options.tools?.map(tool => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters,\n },\n }))\n const resolvedThinking = resolveThinking(options, defaults)\n return {\n model: options.model,\n messages,\n stream: true,\n stream_options: { include_usage: true },\n ...resolvedThinking.reasoningEffort !== undefined\n ? { reasoning_effort: resolvedThinking.reasoningEffort }\n : {},\n ...tools !== undefined && tools.length > 0 ? { tools } : {},\n ...options.temperature !== undefined ? { temperature: options.temperature } : {},\n ...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },\n ...options.stop !== undefined ? { stop: options.stop } : {},\n }\n}\n\n/**\n * Build the full wire request. Always streaming (`stream: true`, usage\n * reporting on); optional fields are omitted rather than sent as null, so\n * provider defaults apply.\n * @param options - the harness request (model, history, system, tools, sampling).\n * @param defaults - adapter-level thinking defaults; undefined fields put nothing on the wire.\n * @returns the chat-completions request body.\n */\nexport function serializeRequest(\n options: GenerateOptions,\n defaults: RequestDefaults = {},\n): WireRequest {\n const messages: WireMessage[] = []\n if (options.system !== undefined) {\n messages.push({ role: 'system', content: options.system })\n }\n messages.push(...serializeMessages(options.model, options.messages))\n\n return requestWithMessages(options, messages, defaults)\n}\n", "/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field' | 'max-buffer-size-exceeded'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n", "/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserConfig} from './types.ts'\n\n// ASCII codes used in the hot parsing paths.\nconst LF = 10\nconst CR = 13\nconst SPACE = 32\n\n// oxlint-disable-next-line no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param config - Parser configuration. Accepts callbacks (see {@link ParserCallbacks})\n * and options like `maxBufferSize` (see {@link ParserConfig}).\n *\n * @returns A new EventSource parser, with `feed` and `reset` methods.\n * @public\n */\nexport function createParser(config: ParserConfig): EventSourceParser {\n if (typeof config === 'function') {\n throw new TypeError(\n '`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment, maxBufferSize} = config\n\n // Trailing bytes from prior `feed()` calls that did not yet form a complete line.\n // Stored as an array of fragments and only joined when a line terminator arrives.\n // Concatenating per-feed (`prefix + chunk`) is O(N²) when a single SSE line spans\n // many chunks (e.g. a large `data:` payload streamed in tiny slices, or an MCP-style\n // server that emits one giant content block). Buffering as fragments + joining once\n // makes the same workload linear.\n const pendingFragments: string[] = []\n\n // Running total of `pendingFragments` lengths, kept in sync with the array so the\n // `maxBufferSize` check doesn't have to walk the fragment list on every feed.\n let pendingFragmentsLength = 0\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let dataLines = 0\n let eventType: string | undefined\n\n // Set after a `maxBufferSize` overflow. Once tripped, `feed()` throws until\n // `reset()` is called — see the comment on `maxBufferSize` in `ParserConfig`.\n let terminated = false\n\n /**\n * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do\n * not yet form a complete line are held back and prepended to the next chunk,\n * so callers can pass arbitrary slices of the stream without worrying about\n * line boundaries.\n *\n * Per the SSE spec, a UTF-8 BOM (0xEF 0xBB 0xBF) at the start of the very\n * first chunk is stripped before parsing.\n *\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream\n */\n function feed(chunk: string) {\n if (terminated) {\n throw new Error(\n 'Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.',\n )\n }\n\n if (isFirstChunk) {\n isFirstChunk = false\n // Match and strip UTF-8 BOM from the start of the stream, if present.\n // (Per the spec, this is only valid at the very start of the stream)\n if (\n chunk.charCodeAt(0) === 0xef &&\n chunk.charCodeAt(1) === 0xbb &&\n chunk.charCodeAt(2) === 0xbf\n ) {\n chunk = chunk.slice(3)\n }\n }\n\n // Hot path: no buffered prefix from a prior partial line. Hand the chunk\n // straight to `processLines`, exactly like the original implementation.\n // Zero new work in the common case (every chunk ends with `\\n\\n`).\n if (pendingFragments.length === 0) {\n const trailing = processLines(chunk)\n if (trailing !== '') {\n pendingFragments.push(trailing)\n pendingFragmentsLength = trailing.length\n }\n checkBufferSize()\n return\n }\n\n // We have a buffered prefix. If this chunk also has no terminator, append\n // to the buffer without concatenating — that's the O(N²) trap we're\n // avoiding (large single `data:` payload split across many tiny chunks).\n if (chunk.indexOf('\\n') === -1 && chunk.indexOf('\\r') === -1) {\n pendingFragments.push(chunk)\n pendingFragmentsLength += chunk.length\n checkBufferSize()\n return\n }\n\n // Terminator arrived. Join the accumulated fragments + this chunk once,\n // process, and buffer any new trailing partial line.\n pendingFragments.push(chunk)\n const input = pendingFragments.join('')\n pendingFragments.length = 0\n pendingFragmentsLength = 0\n const trailing = processLines(input)\n if (trailing !== '') {\n pendingFragments.push(trailing)\n pendingFragmentsLength = trailing.length\n }\n checkBufferSize()\n }\n\n function checkBufferSize() {\n if (maxBufferSize === undefined) return\n if (pendingFragmentsLength + data.length <= maxBufferSize) return\n\n terminated = true\n pendingFragments.length = 0\n pendingFragmentsLength = 0\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n onError(\n new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, {\n type: 'max-buffer-size-exceeded',\n }),\n )\n }\n\n /**\n * Splits `chunk` into SSE lines and dispatches each to the appropriate handler.\n * Returns any trailing bytes that did not terminate with a line break, so the\n * caller can prepend them to the next chunk.\n *\n * The SSE spec permits three line terminators: `\\n`, `\\r`, and `\\r\\n`. Real-world\n * streams almost always use plain `\\n`, so we take a fast path when no `\\r` is\n * present in the chunk. The slow path is spec-correct but does more work per line.\n */\n function processLines(chunk: string): string {\n let searchIndex = 0\n\n // Fast path: LF-only chunk (the common case for typical SSE servers).\n // We can scan forward with a single `indexOf('\\n')` per line and inline\n // the hot-path branches for `data:` and `event:` without the CR bookkeeping\n // the slow path needs.\n if (chunk.indexOf('\\r') === -1) {\n let lfIndex = chunk.indexOf('\\n', searchIndex)\n while (lfIndex !== -1) {\n // Blank line: end-of-event marker. Dispatch the accumulated event (if any)\n // and reset the buffered fields. This is hoisted out of `parseLine` because\n // it's the single most common line shape after `data:` lines.\n if (searchIndex === lfIndex) {\n if (dataLines > 0) {\n onEvent({id, event: eventType, data})\n }\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n const firstCharCode = chunk.charCodeAt(searchIndex)\n if (isDataPrefix(chunk, searchIndex, firstCharCode)) {\n // `data:` line — append the value to the event's data buffer.\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart =\n chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5\n const value = chunk.slice(valueStart, lfIndex)\n // Fast path within a fast path: if this is the first data line AND the\n // next char is another LF (i.e. `data:foo\\n\\n`), dispatch immediately\n // without ever writing to the `data` buffer. This is the shape of a\n // typical single-line SSE event (ChatGPT-style streams, etc.) and is\n // hot enough to be worth the duplication.\n if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {\n onEvent({id, event: eventType, data: value})\n id = undefined\n data = ''\n eventType = undefined\n searchIndex = lfIndex + 2\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n // Multi-line data: concatenate with newline separator per spec.\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {\n // `event:` line — set the event type for the next dispatch. Per spec,\n // an empty value resets `event type` to its default (undefined here).\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(\n chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,\n lfIndex,\n ) || undefined\n } else {\n // Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown\n // fields, or malformed lines. These are rarer and go through the full\n // per-line parser, which handles the SSE field grammar in detail.\n parseLine(chunk, searchIndex, lfIndex)\n }\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n }\n return chunk.slice(searchIndex)\n }\n\n // Slow path: the chunk contains at least one `\\r`, so lines may be terminated\n // by `\\r`, `\\n`, or `\\r\\n`. We locate the next terminator by looking at both\n // the nearest `\\r` and `\\n` and picking whichever comes first.\n while (searchIndex < chunk.length) {\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n lineEnd = crIndex < lfIndex ? crIndex : lfIndex\n } else if (crIndex !== -1) {\n // A trailing `\\r` at the very end of the chunk is ambiguous: it could be\n // a bare-CR terminator, or the first half of a `\\r\\n` whose `\\n` arrives\n // in the next chunk. Defer until we see more input.\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n if (lineEnd === -1) {\n break\n }\n\n parseLine(chunk, searchIndex, lineEnd)\n searchIndex = lineEnd + 1\n // If we just consumed a `\\r` and the next char is `\\n`, skip it so the\n // pair is treated as a single terminator rather than an empty line.\n if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {\n searchIndex++\n }\n }\n\n return chunk.slice(searchIndex)\n }\n\n function parseLine(chunk: string, start: number, end: number) {\n if (start === end) {\n dispatchEvent()\n return\n }\n\n const firstCharCode = chunk.charCodeAt(start)\n\n if (isDataPrefix(chunk, start, firstCharCode)) {\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5\n const value = chunk.slice(valueStart, end)\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n return\n }\n\n if (isEventPrefix(chunk, start, firstCharCode)) {\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined\n return\n }\n\n // Fast path for \"id:\" — 'i' = 105, 'd' = 100, ':' = 58\n if (\n firstCharCode === 105 &&\n chunk.charCodeAt(start + 1) === 100 &&\n chunk.charCodeAt(start + 2) === 58\n ) {\n // 'id:'.length === 3, 'id: '.length === 4\n const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end)\n id = value.includes('\\0') ? undefined : value\n return\n }\n\n // Comment line — ':' = 58\n if (firstCharCode === 58) {\n if (onComment) {\n const line = chunk.slice(start, end)\n // skip ':' (+1), or ': ' (+2) when a space follows\n onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1))\n }\n return\n }\n\n const line = chunk.slice(start, end)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex === -1) {\n processField(line, '', line)\n return\n }\n\n const field = line.slice(0, fieldSeparatorIndex)\n // skip ':' (+1), or ': ' (+2) when a space follows\n const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n processField(field, value, line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value || undefined\n break\n case 'data':\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n if (dataLines > 0) {\n onEvent({\n id,\n event: eventType,\n data,\n })\n }\n\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (options.consume && pendingFragments.length > 0) {\n const incompleteLine = pendingFragments.join('')\n parseLine(incompleteLine, 0, incompleteLine.length)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n pendingFragments.length = 0\n pendingFragmentsLength = 0\n terminated = false\n }\n\n return {feed, reset}\n}\n\n/**\n * Checks if `chunk` starts with the literal `data:` at index `i`.\n *\n * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this\n * hand-unrolled char-code comparison is ~20% faster on common event types.\n * The caller passes `firstCharCode` (the code at `i`) so it can be reused\n * across prefix checks.\n *\n * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58\n */\nfunction isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 100 &&\n chunk.charCodeAt(i + 1) === 97 &&\n chunk.charCodeAt(i + 2) === 116 &&\n chunk.charCodeAt(i + 3) === 97 &&\n chunk.charCodeAt(i + 4) === 58\n )\n}\n\n/**\n * Checks if `chunk` starts with the literal `event:` at index `i`.\n *\n * See {@link isDataPrefix} for why this is hand-unrolled rather than using\n * `String.prototype.startsWith`.\n *\n * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58\n */\nfunction isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 101 &&\n chunk.charCodeAt(i + 1) === 118 &&\n chunk.charCodeAt(i + 2) === 101 &&\n chunk.charCodeAt(i + 3) === 110 &&\n chunk.charCodeAt(i + 4) === 116 &&\n chunk.charCodeAt(i + 5) === 58\n )\n}\n", "import {createParser} from './parse.ts'\nimport type {EventSourceMessage, EventSourceParser} from './types.ts'\n\n/**\n * Options for the EventSourceParserStream.\n *\n * @public\n */\nexport interface StreamOptions {\n /**\n * Behavior when a parsing error occurs.\n *\n * - A custom function can be provided to handle the error.\n * - `'terminate'` will error the stream and stop parsing.\n * - Any other value will ignore the error and continue parsing.\n *\n * @defaultValue `undefined`\n */\n onError?: ('terminate' | ((error: Error) => void)) | undefined\n\n /**\n * Callback for when a reconnection interval is sent from the server.\n *\n * @param retry - The number of milliseconds to wait before reconnecting.\n */\n onRetry?: ((retry: number) => void) | undefined\n\n /**\n * Callback for when a comment is encountered in the stream.\n *\n * @param comment - The comment encountered in the stream.\n */\n onComment?: ((comment: string) => void) | undefined\n\n /**\n * Maximum number of characters the parser is allowed to buffer across calls to `feed()`.\n * See {@link ParserConfig.maxBufferSize} for details.\n *\n * When the limit is exceeded, the stream is always errored (regardless of the `onError`\n * setting) since the underlying parser is unrecoverable without a `reset()`.\n *\n * @defaultValue `undefined` (unbounded)\n */\n maxBufferSize?: number | undefined\n}\n\n/**\n * A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.\n *\n * @example Basic usage\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream())\n * ```\n *\n * @example Terminate stream on parsing errors\n * ```\n * const eventStream =\n * response.body\n * .pipeThrough(new TextDecoderStream())\n * .pipeThrough(new EventSourceParserStream({onError: 'terminate'}))\n * ```\n *\n * @public\n */\nexport class EventSourceParserStream extends TransformStream<string, EventSourceMessage> {\n constructor({onError, onRetry, onComment, maxBufferSize}: StreamOptions = {}) {\n let parser!: EventSourceParser\n\n super({\n start(controller) {\n parser = createParser({\n onEvent: (event) => {\n controller.enqueue(event)\n },\n onError(error) {\n if (typeof onError === 'function') {\n onError(error)\n }\n\n // `max-buffer-size-exceeded` is fatal — the parser is unusable until\n // `reset()`, which the stream wrapper has no way to call. Always\n // terminate the stream in that case so consumers see the meaningful\n // `ParseError` instead of an opaque \"cannot feed terminated parser\"\n // throw from the next chunk.\n if (onError === 'terminate' || error.type === 'max-buffer-size-exceeded') {\n controller.error(error)\n }\n\n // Ignore by default\n },\n onRetry,\n onComment,\n maxBufferSize,\n })\n },\n transform(chunk) {\n parser.feed(chunk)\n },\n })\n }\n}\n\nexport {type ErrorType, ParseError} from './errors.ts'\nexport type {EventSourceMessage} from './types.ts'\n", "/**\n * Decode an SSE byte stream into event `data` payloads. Framing \u2014 chunk\n * reassembly, UTF-8/CRLF/BOM handling, comment and non-data field skipping,\n * multi-`data:` joining \u2014 is `eventsource-parser`'s. Comments are reported\n * only through an optional transport-activity callback. This module keeps the\n * OpenAI-compatible protocol: the literal `[DONE]` is yielded so the caller\n * owns final flushing, and EOF before it raises {@link LlmError}. Framing is\n * spec-strict: an event dispatches only on its blank-line terminator, so an\n * unterminated tail at EOF is truncation, not a flushable payload.\n *\n * @module dsh-llm-ollama-cloud/sse\n */\n\nimport { EventSourceParserStream } from 'eventsource-parser/stream'\nimport { LlmError } from '@deepseek-ai/dsh-llm'\n\n/** The terminal payload OpenAI-compatible endpoints send after the last chunk. */\nexport const DONE = '[DONE]'\n\n/**\n * Parse an SSE byte stream into data payloads. Yields `[DONE]` as the final\n * value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends\n * without it (truncated response \u2014 the model call cannot be trusted).\n * @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence.\n * @param onComment - optional transport-activity callback; comments never enter the yielded payload stream.\n * @returns each event's data payload in arrival order, the `[DONE]` sentinel last.\n */\nexport async function* parseSse(\n stream: ReadableStream<BufferSource>,\n onComment?: (comment: string) => void,\n): AsyncGenerator<string> {\n const events = stream\n .pipeThrough(new TextDecoderStream())\n .pipeThrough(new EventSourceParserStream({ onComment }))\n for await (const { data } of events) {\n yield data\n if (data === DONE) return\n }\n throw new LlmError('SSE stream ended without [DONE]', 'STREAM_CLOSED')\n}\n", "/**\n * Translate Ollama wire chunks into the harness `StreamChunk` protocol with\n * one stateful harness block per content, reasoning, or tool call index. An\n * empty initial reasoning delta does not open a block. Finish reason and the\n * latest usage are deferred until `[DONE]`, covering both finish-attached and\n * trailing usage-only shapes while ensuring no chunk follows `finish`.\n * @module dsh-llm-ollama-cloud/translate\n */\n\nimport { EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm'\nimport type { ContentBlock, FinishReason, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm'\nimport type { Branded } from '@deepseek-ai/dsh-brand'\nimport { DONE } from './sse.ts'\nimport type { WireChunk, WireUsage } from './types.ts'\n\n/**\n * Brand a provider-issued tool call id. dsh-llm's export name differs across\n * harness versions (`ToolCallId` vs `CallId`), so the plugin brands locally\n * with the shared dsh-brand primitive; the brand is type-only, and the runtime\n * value is the plain string either version accepts.\n */\ntype ToolCallId = Branded<'ToolCallId'>\nfunction ToolCallId(id: string): ToolCallId {\n return id as ToolCallId\n}\n\n/** One open block under assembly. */\ninterface OpenBlock {\n index: number\n kind: 'text' | 'reasoning' | 'tool-call'\n text: string\n /** tool-call only */\n callId?: string\n name?: string\n}\n\n/**\n * Map the wire finish_reason vocabulary to the harness FinishReason.\n * @param reason - the wire `finish_reason` string.\n * @returns the mapped reason; unrecognized values (content_filter, \u2026) become `{kind: 'error'}` with the uppercased value as `code`.\n */\nexport function mapFinishReason(reason: string): FinishReason {\n switch (reason) {\n case 'stop': return { kind: 'stop' }\n case 'tool_calls': return { kind: 'tool-calls' }\n case 'length': return { kind: 'max-tokens' }\n default:\n // content_filter, insufficient_system_resource, future additions.\n return {\n kind: 'error',\n failure: { message: `model stopped: ${reason}`, code: reason.toUpperCase() },\n }\n }\n}\n\n/**\n * Map wire usage fields to the harness convention of DISJOINT counts. Cache\n * hits and reasoning tokens are carried only when the wire reported them.\n * @param usage - wire usage from the finish chunk or the trailing usage-only chunk.\n * @returns disjoint harness counts; cache/reasoning fields present only when the wire reported them.\n */\nexport function mapUsage(usage: WireUsage): TokenUsage {\n const cacheRead = usage.prompt_tokens_details?.cached_tokens\n const reasoning = usage.completion_tokens_details?.reasoning_tokens\n return {\n inputTokens: usage.prompt_tokens - (cacheRead ?? 0),\n outputTokens: usage.completion_tokens,\n ...cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {},\n ...reasoning !== undefined ? { reasoningTokens: reasoning } : {},\n }\n}\n\n/** Assemble the final ContentBlock for one open block. */\nfunction closeBlock(block: OpenBlock): ContentBlock {\n switch (block.kind) {\n case 'text': return { type: 'text', text: block.text }\n case 'reasoning': return { type: 'reasoning', text: block.text }\n case 'tool-call': return {\n type: 'tool-call',\n id: ToolCallId(block.callId ?? ''),\n name: block.name ?? '',\n arguments: block.text,\n }\n }\n}\n\n/**\n * Consume SSE data payloads (ending with `[DONE]`) and yield StreamChunks.\n * Malformed JSON payloads abort the stream with `MALFORMED_RESPONSE`.\n * @param payloads - SSE data payloads from {@link parseSse}, `[DONE]`-terminated.\n * @returns deltas as they arrive; `block-end`s, `usage`, and `finish` are all deferred to the `[DONE]` sentinel.\n * A `stop` (or absent) finish with no opened blocks is a degenerate provider completion and maps to an\n * `EMPTY_RESPONSE` error finish instead of a successful empty message.\n */\nexport async function* translate(payloads: AsyncIterable<string>): AsyncGenerator<StreamChunk> {\n let nextIndex = 0\n let textBlock: OpenBlock | undefined\n let reasoningBlock: OpenBlock | undefined\n const toolBlocks = new Map<number, OpenBlock>()\n const order: OpenBlock[] = []\n let pendingFinish: FinishReason | undefined\n let pendingUsage: TokenUsage | undefined\n\n function open(kind: OpenBlock['kind']): OpenBlock {\n const block: OpenBlock = { index: nextIndex++, kind, text: '' }\n order.push(block)\n return block\n }\n\n for await (const payload of payloads) {\n if (payload === DONE) {\n for (const block of order) {\n yield { type: 'block-end', index: block.index, block: closeBlock(block) }\n }\n if (pendingUsage) yield { type: 'usage', usage: pendingUsage }\n const reason = pendingFinish ?? { kind: 'stop' as const }\n yield {\n type: 'finish',\n reason: reason.kind === 'stop' && order.length === 0\n ? {\n kind: 'error',\n failure: { message: 'model returned a completed response with no content', code: EMPTY_RESPONSE_CODE },\n }\n : reason,\n }\n return\n }\n\n let chunk: WireChunk\n try {\n chunk = JSON.parse(payload) as WireChunk\n } catch {\n throw new LlmError(`malformed SSE payload: ${payload.slice(0, 120)}`, 'MALFORMED_RESPONSE')\n }\n\n for (const choice of chunk.choices ?? []) {\n const delta = choice.delta\n\n // Reasoning first: thinking mode interleaves it before text. The\n // empty-string first chunk must not open a block.\n const reasoning = delta?.reasoning\n if (typeof reasoning === 'string' && reasoning.length > 0) {\n if (!reasoningBlock) {\n reasoningBlock = open('reasoning')\n yield { type: 'block-start', index: reasoningBlock.index, blockType: 'reasoning' }\n }\n reasoningBlock.text += reasoning\n yield { type: 'reasoning-delta', index: reasoningBlock.index, text: reasoning }\n }\n\n const content = delta?.content\n if (typeof content === 'string' && content.length > 0) {\n if (!textBlock) {\n textBlock = open('text')\n yield { type: 'block-start', index: textBlock.index, blockType: 'text' }\n }\n textBlock.text += content\n yield { type: 'text-delta', index: textBlock.index, text: content }\n }\n\n for (const call of delta?.tool_calls ?? []) {\n let block = toolBlocks.get(call.index)\n if (!block) {\n block = open('tool-call')\n toolBlocks.set(call.index, block)\n yield { type: 'block-start', index: block.index, blockType: 'tool-call' }\n }\n if (call.id !== undefined) block.callId = call.id\n if (call.function?.name !== undefined) block.name = call.function.name\n const fragment = call.function?.arguments ?? ''\n block.text += fragment\n yield {\n type: 'tool-call-delta',\n index: block.index,\n id: ToolCallId(block.callId ?? ''),\n ...block.name !== undefined ? { name: block.name } : {},\n argumentsDelta: fragment,\n }\n }\n\n if (typeof choice.finish_reason === 'string') {\n pendingFinish = mapFinishReason(choice.finish_reason)\n }\n }\n\n // Usage may arrive attached to the finish chunk or as a trailing\n // usage-only chunk \u2014 keep the latest.\n if (chunk.usage) pendingUsage = mapUsage(chunk.usage)\n }\n\n // parseSse guarantees the [DONE] sentinel (or throws); reaching here means\n // the payload source violated that contract.\n throw new LlmError('SSE payload stream ended without [DONE]', 'STREAM_CLOSED')\n}\n", "/**\n * Answering \"which models can this draft serve?\" for the Models settings\n * page's fetch action.\n *\n * A draft naming this plugin's route is answered **from the adapter's own\n * catalog**, with no network call: the resolved section is the authoritative\n * list for the route, and it carries the cloud-suffixed ids requests actually\n * use. Only a draft carrying an endpoint \u2014 a gateway or OpenAI-compatible\n * mirror the catalog says nothing about \u2014 is interrogated over the wire at\n * `GET {baseURL}/models`, the one listing shape such endpoints agree on.\n *\n * Nothing here is stored: the request carries a draft the user is still\n * editing, and the reply is candidate metadata the surface offers for\n * adoption. The section remains the only thing that decides what the route\n * serves.\n *\n * @module llm-ollama-cloud/discovery\n */\n\nimport { INVALID_CREDENTIAL_CODE, LlmError, normalizeApiKey } from '@deepseek-ai/dsh-llm'\nimport type { LlmDiscoveredModel, LlmModelDiscoveryOperation } from '@deepseek-ai/dsh-llm'\nimport { attributionHeaders } from '@deepseek-ai/dsh-llm'\nimport type { OllamaCatalogModel } from './adapter.ts'\n\n/**\n * Endpoint replies larger than this are refused. The endpoint is whatever URL\n * the user typed, so the ceiling holds on the bytes actually read rather than\n * on the length the server claims.\n */\nconst MAX_RESPONSE_BYTES = 4 * 1024 * 1024\n\n/** One entry of an OpenAI-compatible `GET /models` reply. */\ninterface ListingEntry {\n id?: unknown\n /** Common gateway extensions; absent from the official listing. */\n name?: unknown\n display_name?: unknown\n context_window?: unknown\n context_length?: unknown\n max_tokens?: unknown\n max_output_tokens?: unknown\n}\n\n/** A positive integer field of a listing entry, or `undefined` when absent or unusable. */\nfunction capacity(...candidates: readonly unknown[]): number | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'number' && Number.isInteger(candidate) && candidate > 0) return candidate\n }\n return undefined\n}\n\n/** A non-empty string field of a listing entry, or `undefined`. */\nfunction label(...candidates: readonly unknown[]): string | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.length > 0) return candidate\n }\n return undefined\n}\n\n/**\n * Join the endpoint base with the listing path. The base is treated as a\n * prefix rather than a URL to resolve against, so a deployment path such as\n * `https://gateway.example/openai/v1` keeps its segments instead of losing\n * them to `URL` resolution.\n */\nfunction listingUrl(baseURL: string): string {\n return `${baseURL.replace(/\\/+$/, '')}/models`\n}\n\n/**\n * Accept one probe key, or refuse it before the header is built. Without this\n * the `fetch` below would throw a ByteString `TypeError` that the transport\n * catch reports as `could not reach <url>` \u2014 blaming the network for a local,\n * deterministic fault.\n * @param raw - the key typed into the form or read from storage.\n * @returns the trimmed, usable key.\n */\nfunction usableProbeKey(raw: string): string {\n const checked = normalizeApiKey(raw)\n if (checked.ok) return checked.value\n throw new LlmError(\n checked.reason === 'empty'\n ? 'this provider\\'s API key is blank; enter it on the Models page, or clear it to probe unauthenticated'\n : 'this provider\\'s API key contains characters no HTTP header can carry; paste the raw key only',\n INVALID_CREDENTIAL_CODE,\n )\n}\n\n/**\n * Read a reply body, refusing one that outgrows the ceiling. A declared length\n * is checked first so an honest server is turned away without transferring\n * anything; the accumulated total is what actually enforces the bound, because\n * a server that under-declares (or streams) tells us nothing up front.\n */\nasync function readBounded(response: Response, url: string): Promise<string> {\n const oversized = (): LlmError =>\n new LlmError(`${url} answered with more than ${MAX_RESPONSE_BYTES} bytes`, 'DISCOVERY_FAILED')\n const declared = Number(response.headers.get('content-length') ?? Number.NaN)\n if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {\n await response.body?.cancel()\n throw oversized()\n }\n if (response.body === null) return ''\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n total += value.byteLength\n if (total > MAX_RESPONSE_BYTES) throw oversized()\n chunks.push(value)\n }\n } finally {\n /* v8 ignore next 4 -- cancel() after a completed or abandoned read settles without rejecting; unobserved best-effort cleanup. */\n await reader.cancel().catch(() => {\n // Cancel after a drained read, or after this function walked away from\n // an oversized one, is cleanup; the reply is already decided either way.\n })\n }\n const body = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n body.set(chunk, offset)\n offset += chunk.byteLength\n }\n return new TextDecoder().decode(body)\n}\n\n/**\n * Read one OpenAI-compatible listing reply. Entries without a usable id are\n * skipped rather than failing the whole interrogation: a single malformed row\n * should not deny the user the rest of a working endpoint's catalog.\n */\nfunction readListing(body: unknown): LlmDiscoveredModel[] {\n const data = (body as { data?: unknown } | null)?.data\n if (!Array.isArray(data)) {\n throw new LlmError(\n 'the endpoint\\'s model listing has no \"data\" array; enter this provider\\'s models by hand',\n 'DISCOVERY_FAILED',\n )\n }\n const models: LlmDiscoveredModel[] = []\n for (const raw of data) {\n const entry = raw as ListingEntry | null\n const id = label(entry?.id)\n if (id === undefined) continue\n const name = label(entry?.name, entry?.display_name)\n const contextWindow = capacity(entry?.context_window, entry?.context_length)\n const maxTokens = capacity(entry?.max_output_tokens, entry?.max_tokens)\n models.push({\n id,\n ...name === undefined ? {} : { name },\n ...contextWindow === undefined ? {} : { contextWindow },\n ...maxTokens === undefined ? {} : { maxTokens },\n })\n }\n return models\n}\n\n/**\n * Interrogate one draft provider for the models it advertises.\n * @param request - the endpoint and one-shot credential to use.\n * @param installed - the route's own catalog as currently resolved; the\n * answer for a draft naming the route.\n * @param storedApiKey - the credential the stored section resolves, asked for\n * only when the draft carries none and only on the path that reaches the\n * network. A configuration surface never holds a stored secret \u2014 it edits a\n * redacted descriptor \u2014 so without this an already-configured route would be\n * interrogated unauthenticated and answer 401.\n * @returns the advertised models in endpoint order.\n * @throws LlmError when the draft names neither a catalog route nor an\n * endpoint, the endpoint refuses or fails the request, or the reply is not\n * a model listing.\n */\nexport async function discoverModels(\n request: LlmModelDiscoveryOperation,\n installed: readonly OllamaCatalogModel[],\n storedApiKey?: () => Promise<string | undefined>,\n): Promise<readonly LlmDiscoveredModel[]> {\n // A named route already has its answer, and a better one: the installed\n // entries carry the cloud-suffixed ids and capacities no listing endpoint\n // reports, and they are already normalized through the same step the\n // adapter's own requests go through.\n if (request.provider !== undefined && installed.length > 0) {\n return installed.map(model => ({\n id: model.id,\n name: model.name ?? model.id,\n ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },\n ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },\n }))\n }\n if (request.baseURL === undefined || request.baseURL.length === 0) {\n throw new LlmError(\n 'model discovery needs a baseURL to interrogate; set one, or enter this provider\\'s models by hand',\n 'DISCOVERY_FAILED',\n )\n }\n const url = listingUrl(request.baseURL)\n // A key typed into the form wins: it is the one the user is testing, and it\n // may be the replacement for exactly the stored key that is failing. The\n // stored one is only asked for here, past the catalog short-circuit, so a\n // route answered from the registry costs no credential lookup \u2014 and no\n // diagnostic about a credential it never needed. A probe carrying no key\n // stays unauthenticated, which is how an auth-free gateway is meant to be\n // asked.\n const supplied = request.apiKey ?? await storedApiKey?.()\n const apiKey = supplied === undefined ? undefined : usableProbeKey(supplied)\n let response: Response\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n ...apiKey === undefined ? {} : { authorization: `Bearer ${apiKey}` },\n ...attributionHeaders(),\n },\n ...request.signal === undefined ? {} : { signal: request.signal },\n })\n } catch (error: unknown) {\n if (request.signal?.aborted) {\n throw new LlmError('model discovery aborted by caller', 'ABORTED', { cause: error })\n }\n throw new LlmError(`could not reach ${url}`, 'DISCOVERY_FAILED', { cause: error })\n }\n if (!response.ok) {\n throw new LlmError(\n `${url} answered ${response.status}${response.status === 401 || response.status === 403 ? '; check the API key' : ''}`,\n 'DISCOVERY_FAILED',\n )\n }\n let text: string\n try {\n text = await readBounded(response, url)\n } catch (error: unknown) {\n // Cancellation during the body read rejects with the abort reason, which\n // may be any value; the caller gets the same coded failure it would have\n // for a cancellation before the request went out.\n if (request.signal?.aborted) {\n throw new LlmError('model discovery aborted by caller', 'ABORTED', { cause: error })\n }\n throw error\n }\n let body: unknown\n try {\n body = JSON.parse(text)\n } catch (error: unknown) {\n throw new LlmError(`${url} did not answer with JSON`, 'DISCOVERY_FAILED', { cause: error })\n }\n return readListing(body)\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["LlmError", "contentHasImage", "LlmError", "trailing", "value", "line", "LlmError", "LlmError", "LlmError", "delay", "contentHasImage", "LlmError", "LlmError", "attributionHeaders", "name", "LlmError"]
|
|
7
7
|
}
|
package/package.json
CHANGED
package/src/adapter.ts
CHANGED
|
@@ -237,7 +237,7 @@ export class OllamaAdapter extends LlmAdapter {
|
|
|
237
237
|
override providerInfo(provider: string): LlmProviderInfo {
|
|
238
238
|
// Matches the configurable-provider directory's displayName, so the
|
|
239
239
|
// Models page row and the model-picker group name read as one provider.
|
|
240
|
-
return { id: provider, name: '
|
|
240
|
+
return { id: provider, name: 'ollama-cloud' }
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
override providerRetryPolicy(_provider: string): ResolvedRetryPolicy {
|
package/src/index.ts
CHANGED
|
@@ -319,7 +319,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
319
319
|
// Declared even while dormant, so configuration surfaces list the route in
|
|
320
320
|
// the add-provider select before any profile exists.
|
|
321
321
|
ctx.llm.registerConfigurableProviders([
|
|
322
|
-
{ provider: PROVIDER, displayName: '
|
|
322
|
+
{ provider: PROVIDER, displayName: 'ollama-cloud', settingsNs: NS, settingsPath: ['providers', PROVIDER] },
|
|
323
323
|
])
|
|
324
324
|
// Route effects bind to this apply fiber via the stable `ctx` reference,
|
|
325
325
|
// even when a swap runs inside the scoped settings callback below.
|