dsh-subscriptions 0.1.5 → 0.2.0

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.
Files changed (41) hide show
  1. package/README.md +31 -5
  2. package/lib/auth/rpc.d.ts +73 -2
  3. package/lib/auth/rpc.js +53 -3
  4. package/lib/client/ExternalUsageCards.js +1 -1
  5. package/lib/client/OllamaCard.d.ts +13 -0
  6. package/lib/client/OllamaCard.js +76 -0
  7. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  8. package/lib/client/OllamaSettingsManager.js +246 -0
  9. package/lib/client/ProviderModelEditor.js +4 -2
  10. package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
  11. package/lib/client/SubscriptionUsageBadge.js +3 -2
  12. package/lib/client/SubscriptionsSection.d.ts +2 -0
  13. package/lib/client/SubscriptionsSection.js +2 -1
  14. package/lib/client/UsageMeter.js +11 -7
  15. package/lib/client/locales.d.ts +68 -0
  16. package/lib/client/locales.js +77 -0
  17. package/lib/client/ollama-draft.d.ts +229 -0
  18. package/lib/client/ollama-draft.js +246 -0
  19. package/lib/client/usage-pace.d.ts +7 -0
  20. package/lib/client/usage-pace.js +15 -0
  21. package/lib/client.js +1385 -244
  22. package/lib/client.js.map +1 -1
  23. package/lib/index.d.ts +25 -0
  24. package/lib/index.js +3191 -2133
  25. package/lib/provider-settings.d.ts +2 -1
  26. package/lib/provider-settings.js +6 -4
  27. package/lib/providers/claude.d.ts +14 -1
  28. package/lib/providers/claude.js +19 -7
  29. package/lib/providers/common.d.ts +14 -0
  30. package/lib/providers/common.js +48 -0
  31. package/lib/providers/external-usage-controller.d.ts +9 -2
  32. package/lib/providers/external-usage-controller.js +19 -5
  33. package/lib/providers/external-usage.d.ts +8 -0
  34. package/lib/providers/external-usage.js +92 -8
  35. package/lib/providers/ollama-settings.d.ts +90 -0
  36. package/lib/providers/ollama-settings.js +197 -0
  37. package/lib/providers/ollama-web.d.ts +46 -0
  38. package/lib/providers/ollama-web.js +213 -0
  39. package/lib/providers/ollama.d.ts +107 -0
  40. package/lib/providers/ollama.js +545 -0
  41. package/package.json +1 -1
@@ -0,0 +1,197 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
5
+ function fail(message) {
6
+ throw new Error(message);
7
+ }
8
+ function readNonEmptyString(value, field) {
9
+ if (typeof value !== 'string' || value.trim().length === 0)
10
+ fail(`${field} must be a non-empty string`);
11
+ return value;
12
+ }
13
+ function readPositiveInteger(value, field) {
14
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
15
+ fail(`${field} must be a positive integer`);
16
+ }
17
+ return value;
18
+ }
19
+ function readBoolean(value, field) {
20
+ if (typeof value !== 'boolean')
21
+ fail(`${field} must be a boolean`);
22
+ return value;
23
+ }
24
+ /**
25
+ * Accept only an absolute http(s) base URL with no credentials, query, or
26
+ * fragment, and return its trimmed form. The route appends `/tags`, `/show`,
27
+ * and `/v1/chat/completions` to this value, so anything else would build a URL
28
+ * that cannot work — and a non-official host receives the referenced
29
+ * credential, which the dialog tells the user.
30
+ * @param value - the candidate URL.
31
+ * @returns the trimmed URL to store.
32
+ * @throws when the value is not a usable absolute http(s) URL.
33
+ */
34
+ export function validateOllamaBaseURL(value) {
35
+ const raw = readNonEmptyString(value, 'baseURL');
36
+ let url;
37
+ try {
38
+ url = new URL(raw);
39
+ }
40
+ catch {
41
+ fail('baseURL must be an absolute URL');
42
+ }
43
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
44
+ fail('baseURL must use http or https');
45
+ if (url.username !== '' || url.password !== '')
46
+ fail('baseURL must not carry credentials');
47
+ if (url.search !== '' || url.hash !== '')
48
+ fail('baseURL must not carry a query or fragment');
49
+ return raw.trim();
50
+ }
51
+ /** Credential references are looked up by name, so whitespace is always a typo. */
52
+ function readApiKeyRef(value) {
53
+ const raw = readNonEmptyString(value, 'apiKeyRef');
54
+ if (/\s/.test(raw))
55
+ fail('apiKeyRef must not contain whitespace');
56
+ return raw;
57
+ }
58
+ function readEfforts(value, field) {
59
+ if (!Array.isArray(value) || value.length === 0)
60
+ fail(`${field} must be a non-empty array`);
61
+ const efforts = [];
62
+ for (const entry of value) {
63
+ const effort = readNonEmptyString(entry, `${field} entries`);
64
+ if (!efforts.includes(effort))
65
+ efforts.push(effort);
66
+ }
67
+ return efforts;
68
+ }
69
+ function readModel(entry, index) {
70
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
71
+ fail(`models[${index}] must be an object`);
72
+ }
73
+ const record = entry;
74
+ // The id is the wire id, so surrounding whitespace is always a paste
75
+ // artifact; trim it before the duplicate check sees it.
76
+ const model = { id: readNonEmptyString(record.id, `models[${index}].id`).trim() };
77
+ if (record.name !== undefined)
78
+ model.name = readNonEmptyString(record.name, `models[${index}].name`);
79
+ if (record.contextWindow !== undefined) {
80
+ model.contextWindow = readPositiveInteger(record.contextWindow, `models[${index}].contextWindow`);
81
+ }
82
+ if (record.contextOverride !== undefined) {
83
+ model.contextOverride = readPositiveInteger(record.contextOverride, `models[${index}].contextOverride`);
84
+ }
85
+ if (record.maxTokens !== undefined) {
86
+ model.maxTokens = readPositiveInteger(record.maxTokens, `models[${index}].maxTokens`);
87
+ }
88
+ if (record.vision !== undefined)
89
+ model.vision = readBoolean(record.vision, `models[${index}].vision`);
90
+ if (record.thinking !== undefined)
91
+ model.thinking = readBoolean(record.thinking, `models[${index}].thinking`);
92
+ if (record.efforts !== undefined)
93
+ model.efforts = readEfforts(record.efforts, `models[${index}].efforts`);
94
+ if (record.defaultEffort !== undefined) {
95
+ const effort = readNonEmptyString(record.defaultEffort, `models[${index}].defaultEffort`);
96
+ // The runtime rejects a default the model does not advertise, so a
97
+ // mismatch is a store bug rather than something to carry forward.
98
+ if (model.efforts !== undefined && !model.efforts.includes(effort)) {
99
+ fail(`models[${index}].defaultEffort must be one of its efforts`);
100
+ }
101
+ model.defaultEffort = effort;
102
+ }
103
+ return model;
104
+ }
105
+ /**
106
+ * Validate one settings document, dropping unknown keys and duplicate model
107
+ * ids (first wins, matching the Cursor store's visible-model handling).
108
+ * @param input - the parsed store document or an RPC payload.
109
+ * @throws when a known field has the wrong type or an unusable value.
110
+ */
111
+ export function validateOllamaSettings(input) {
112
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
113
+ fail('Ollama settings must be an object');
114
+ }
115
+ const record = input;
116
+ const settings = {};
117
+ if (record.models !== undefined) {
118
+ if (!Array.isArray(record.models))
119
+ fail('models must be an array');
120
+ const seen = new Set();
121
+ const models = [];
122
+ for (const [index, entry] of record.models.entries()) {
123
+ const model = readModel(entry, index);
124
+ if (seen.has(model.id))
125
+ continue;
126
+ seen.add(model.id);
127
+ models.push(model);
128
+ }
129
+ settings.models = models;
130
+ }
131
+ if (record.web !== undefined)
132
+ settings.web = readBoolean(record.web, 'web');
133
+ if (record.baseURL !== undefined)
134
+ settings.baseURL = validateOllamaBaseURL(record.baseURL);
135
+ if (record.apiKeyRef !== undefined)
136
+ settings.apiKeyRef = readApiKeyRef(record.apiKeyRef);
137
+ return settings;
138
+ }
139
+ /**
140
+ * Durable Ollama Cloud configuration under the DSH home directory. Writes are
141
+ * serialized and atomic (temp file with mode 0600, then rename), so a crash
142
+ * never leaves a half-written document and a concurrent writer cannot lose an
143
+ * earlier save.
144
+ */
145
+ export class OllamaSettingsStore {
146
+ path;
147
+ onCorrupt;
148
+ current = {};
149
+ writes = Promise.resolve();
150
+ /** Set when the stored file existed but was unreadable or invalid. */
151
+ corrupt;
152
+ constructor(path = dshHomePath('plugins', 'subscriptions', 'ollama-settings.json'), onCorrupt) {
153
+ this.path = path;
154
+ this.onCorrupt = onCorrupt;
155
+ try {
156
+ this.current = validateOllamaSettings(JSON.parse(readFileSync(path, 'utf8')));
157
+ }
158
+ catch (error) {
159
+ if (error.code !== 'ENOENT') {
160
+ // A broken file must not take the whole plugin down: degrade to
161
+ // defaults, keep the file untouched for manual recovery, and let
162
+ // the next save overwrite it.
163
+ this.corrupt = error instanceof Error ? error.message : String(error);
164
+ // The raw V8 message quotes the offending bytes; the log keeps a
165
+ // fixed diagnosis and corruption() carries the detail for callers
166
+ // that surface it outside log lines.
167
+ this.onCorrupt?.(`Ollama settings at ${path} are unreadable and ignored; defaults serve until the next save.`);
168
+ }
169
+ }
170
+ }
171
+ /** Why the stored file is ignored, when it is. */
172
+ corruption() { return this.corrupt; }
173
+ get() { return structuredClone(this.current); }
174
+ /** The stored catalog, or `undefined` when the store carries no override. */
175
+ models() { return this.current.models; }
176
+ web() { return this.current.web; }
177
+ baseURL() { return this.current.baseURL; }
178
+ apiKeyRef() { return this.current.apiKeyRef; }
179
+ set(input) {
180
+ const validated = validateOllamaSettings(input);
181
+ const run = this.writes.then(async () => {
182
+ await mkdir(dirname(this.path), { recursive: true });
183
+ const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
184
+ try {
185
+ await writeFile(temporary, JSON.stringify(validated, null, 2), { mode: 0o600 });
186
+ await rename(temporary, this.path);
187
+ }
188
+ finally {
189
+ await rm(temporary, { force: true });
190
+ }
191
+ this.current = validated;
192
+ this.corrupt = undefined;
193
+ });
194
+ this.writes = run.catch(() => undefined);
195
+ return run;
196
+ }
197
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Ollama-native web capabilities: search and fetch ride the Cloud
3
+ * `/api/web_search` + `/api/web_fetch` endpoints independently of the
4
+ * selected chat model. Both providers share the `ollama-cloud` id because
5
+ * the seam addresses each capability kind separately.
6
+ */
7
+ import type { WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web';
8
+ import type { FetchFn } from './common.js';
9
+ /** Shared provider id for both web capabilities. */
10
+ export declare const OLLAMA_WEB_PROVIDER_ID = "ollama-cloud";
11
+ /** Per-attempt budget; the seam owns overall cancellation. */
12
+ export declare const OLLAMA_WEB_TIMEOUT_MS = 15000;
13
+ /** Search result cap applied at the request layer. */
14
+ export declare const OLLAMA_WEB_MAX_RESULTS = 10;
15
+ /** Response body bound for web payloads. */
16
+ export declare const OLLAMA_WEB_MAX_BYTES: number;
17
+ export interface OllamaWebOptions {
18
+ /** Resolve the API key per call; `undefined` means unconfigured. */
19
+ apiKey: () => Promise<string | undefined>;
20
+ /**
21
+ * Native base URL; web endpoints ride it unchanged (no `/v1` mapping). A
22
+ * function form is read per call so a Settings save applies without a
23
+ * restart.
24
+ */
25
+ baseURL: string | (() => string);
26
+ /** Local gate; absent counts as enabled. Read on every call. */
27
+ enabled?: () => boolean;
28
+ fetchFn?: FetchFn;
29
+ requestTimeoutMs?: number;
30
+ }
31
+ /** Search provider behind DSH's stock web_search tool. */
32
+ export declare class OllamaWebSearchProvider implements WebSearchProvider {
33
+ private readonly options;
34
+ readonly id = "ollama-cloud";
35
+ constructor(options: OllamaWebOptions);
36
+ available(): boolean;
37
+ search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
38
+ }
39
+ /** Fetch provider behind DSH's stock web_fetch tool. */
40
+ export declare class OllamaWebFetchProvider implements WebFetchProvider {
41
+ private readonly options;
42
+ readonly id = "ollama-cloud";
43
+ constructor(options: OllamaWebOptions);
44
+ available(): boolean;
45
+ fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
46
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Ollama-native web capabilities: search and fetch ride the Cloud
3
+ * `/api/web_search` + `/api/web_fetch` endpoints independently of the
4
+ * selected chat model. Both providers share the `ollama-cloud` id because
5
+ * the seam addresses each capability kind separately.
6
+ */
7
+ import { attributionHeaders } from '@deepseek-ai/dsh-llm';
8
+ import { WebError } from '@deepseek-ai/dsh-web';
9
+ import { readBoundedJson } from './common.js';
10
+ import { hostFetch } from '../http.js';
11
+ /** Shared provider id for both web capabilities. */
12
+ export const OLLAMA_WEB_PROVIDER_ID = 'ollama-cloud';
13
+ /** Per-attempt budget; the seam owns overall cancellation. */
14
+ export const OLLAMA_WEB_TIMEOUT_MS = 15_000;
15
+ /** Search result cap applied at the request layer. */
16
+ export const OLLAMA_WEB_MAX_RESULTS = 10;
17
+ /** Response body bound for web payloads. */
18
+ export const OLLAMA_WEB_MAX_BYTES = 2 * 1024 * 1024;
19
+ /** The effective base URL; both forms are read fresh so saves apply live. */
20
+ function baseURLOf(options) {
21
+ return typeof options.baseURL === 'function' ? options.baseURL() : options.baseURL;
22
+ }
23
+ function throwIfAborted(signal) {
24
+ signal?.throwIfAborted();
25
+ }
26
+ function isRedirectFailure(error) {
27
+ // Real fetch failures nest: undici reports `fetch failed` on top with the
28
+ // redirect rejection in `cause`. Walk the chain instead of reading only
29
+ // the outer message.
30
+ for (let current = error; current instanceof Error; current = current.cause) {
31
+ if (/redirect/i.test(current.message))
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ async function postJson(url, apiKey, payload, options, signal) {
37
+ const fetchFn = options.fetchFn ?? hostFetch;
38
+ const timeoutMs = options.requestTimeoutMs ?? OLLAMA_WEB_TIMEOUT_MS;
39
+ // Exactly two attempts; only a timeout or a pre-response transport
40
+ // failure retries. HTTP errors, malformed replies, missing credentials,
41
+ // redirects, and caller cancellation never retry.
42
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
43
+ throwIfAborted(signal);
44
+ const controller = new AbortController();
45
+ let timedOut = false;
46
+ const timer = setTimeout(() => {
47
+ timedOut = true;
48
+ controller.abort(new Error('ollama web request timed out'));
49
+ }, timeoutMs);
50
+ if (typeof timer.unref === 'function') {
51
+ timer.unref();
52
+ }
53
+ const onCallerAbort = () => controller.abort(signal?.reason);
54
+ signal?.addEventListener('abort', onCallerAbort, { once: true });
55
+ try {
56
+ const response = await fetchFn(url, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'content-type': 'application/json',
60
+ 'accept': 'application/json',
61
+ 'authorization': `Bearer ${apiKey}`,
62
+ ...attributionHeaders(),
63
+ },
64
+ body: JSON.stringify(payload),
65
+ redirect: 'error',
66
+ signal: controller.signal,
67
+ });
68
+ if (!response.ok) {
69
+ throw new WebError(`ollama web request failed (HTTP ${String(response.status)})`, 'OLLAMA_WEB_BAD_RESPONSE');
70
+ }
71
+ try {
72
+ return await readBoundedJson(response, 'ollama web request', OLLAMA_WEB_MAX_BYTES);
73
+ }
74
+ catch (error) {
75
+ if (signal?.aborted)
76
+ throw error;
77
+ // A timeout can also land mid-body: same retry treatment as a
78
+ // connect timeout, under the same code.
79
+ if (timedOut) {
80
+ if (attempt >= 2)
81
+ throw new WebError('ollama web request timed out', 'OLLAMA_WEB_TIMEOUT');
82
+ continue;
83
+ }
84
+ // The SyntaxError cause is dropped on purpose: V8 quotes the
85
+ // offending body bytes, which would carry provider text into
86
+ // logs through the chained message. An oversized body lands
87
+ // here too: loud and unretried.
88
+ throw new WebError('ollama web request returned a malformed reply', 'OLLAMA_WEB_BAD_REPLY');
89
+ }
90
+ }
91
+ catch (error) {
92
+ if (signal?.aborted)
93
+ throw error;
94
+ // Classified failures carry their own code and never retry.
95
+ if (error instanceof WebError)
96
+ throw error;
97
+ if (isRedirectFailure(error)) {
98
+ throw new WebError('ollama web request redirected; redirects are not followed', 'OLLAMA_WEB_REDIRECT');
99
+ }
100
+ if (timedOut) {
101
+ if (attempt >= 2)
102
+ throw new WebError('ollama web request timed out', 'OLLAMA_WEB_TIMEOUT', { cause: error });
103
+ continue;
104
+ }
105
+ if (attempt >= 2) {
106
+ throw new WebError('ollama web request transport failed', 'OLLAMA_WEB_TRANSPORT', { cause: error });
107
+ }
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ signal?.removeEventListener('abort', onCallerAbort);
112
+ }
113
+ }
114
+ throw new WebError('ollama web request failed', 'OLLAMA_WEB_TRANSPORT');
115
+ }
116
+ function recordOf(value) {
117
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
118
+ ? value
119
+ : undefined;
120
+ }
121
+ async function resolveWebKey(options, signal) {
122
+ throwIfAborted(signal);
123
+ const apiKey = await options.apiKey();
124
+ if (apiKey === undefined) {
125
+ throw new WebError('Ollama web search requires an Ollama Cloud API key; configure OLLAMA_API_KEY', 'OLLAMA_WEB_MISSING_CREDENTIAL');
126
+ }
127
+ return apiKey;
128
+ }
129
+ /**
130
+ * Refuse a call while the Settings switch is off. The host normally selects
131
+ * only available providers, but a pinned `searchProvider`/`fetchProvider`
132
+ * would otherwise keep routing through a provider the user turned off.
133
+ */
134
+ function assertEnabled(options) {
135
+ if (options.enabled?.() === false) {
136
+ throw new WebError('Ollama web search/fetch is disabled in Settings', 'OLLAMA_WEB_DISABLED');
137
+ }
138
+ }
139
+ /** Search provider behind DSH's stock web_search tool. */
140
+ export class OllamaWebSearchProvider {
141
+ options;
142
+ id = OLLAMA_WEB_PROVIDER_ID;
143
+ constructor(options) {
144
+ this.options = options;
145
+ }
146
+ available() {
147
+ try {
148
+ new URL(baseURLOf(this.options));
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ return this.options.enabled?.() ?? true;
154
+ }
155
+ async search(request, signal) {
156
+ assertEnabled(this.options);
157
+ const apiKey = await resolveWebKey(this.options, signal);
158
+ const base = baseURLOf(this.options).replace(/\/+$/, '');
159
+ const payload = await postJson(`${base}/web_search`, apiKey, {
160
+ query: request.query,
161
+ ...(request.maxResults === undefined ? {} : { max_results: Math.min(request.maxResults, OLLAMA_WEB_MAX_RESULTS) }),
162
+ }, this.options, signal);
163
+ const results = recordOf(payload)?.['results'];
164
+ if (!Array.isArray(results))
165
+ throw new WebError('ollama web search returned no results array', 'OLLAMA_WEB_BAD_REPLY');
166
+ // Official shape per docs: {title, url, content}. The snippet rides
167
+ // `content`, not `snippet`.
168
+ const sources = results.flatMap(entry => {
169
+ const record = recordOf(entry);
170
+ const url = record?.['url'];
171
+ if (typeof url !== 'string' || url.length === 0)
172
+ return [];
173
+ return [{
174
+ url,
175
+ ...(typeof record?.['title'] === 'string' ? { title: record?.['title'] } : {}),
176
+ ...(typeof record?.['content'] === 'string' ? { snippet: record?.['content'] } : {}),
177
+ }];
178
+ });
179
+ return { sources, truncated: false };
180
+ }
181
+ }
182
+ /** Fetch provider behind DSH's stock web_fetch tool. */
183
+ export class OllamaWebFetchProvider {
184
+ options;
185
+ id = OLLAMA_WEB_PROVIDER_ID;
186
+ constructor(options) {
187
+ this.options = options;
188
+ }
189
+ available() {
190
+ try {
191
+ new URL(baseURLOf(this.options));
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ return this.options.enabled?.() ?? true;
197
+ }
198
+ async fetch(request, signal) {
199
+ assertEnabled(this.options);
200
+ const apiKey = await resolveWebKey(this.options, signal);
201
+ const base = baseURLOf(this.options).replace(/\/+$/, '');
202
+ const payload = await postJson(`${base}/web_fetch`, apiKey, { url: request.url }, this.options, signal);
203
+ // Official shape per docs: {title, content, links}. The endpoint
204
+ // answers the retrieval itself, so the request URL is the result URL
205
+ // and a decoded body means success.
206
+ const record = recordOf(payload);
207
+ const content = record?.['content'];
208
+ if (typeof content !== 'string') {
209
+ throw new WebError('ollama web fetch returned a malformed reply', 'OLLAMA_WEB_BAD_REPLY');
210
+ }
211
+ return { url: request.url, statusCode: 200, body: { kind: 'text', content }, truncated: false };
212
+ }
213
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Ollama Cloud provider: a key-based OpenAI-completions route with no OAuth
3
+ * session. The credential arrives per request as a bearer key; chat posts to
4
+ * the configured native baseURL's `/v1` sibling. The route ships no catalog:
5
+ * only models the user stored on the Settings card are served, so an empty
6
+ * store hides the picker entry and rejects every chat model.
7
+ */
8
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
9
+ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
10
+ import type { DiscoveredModel, FetchFn } from './common.js';
11
+ import type { OllamaModelSetting } from './ollama-settings.js';
12
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
13
+ /** Route id owned by this adapter. Not a member of the OAuth `PROVIDER_IDS`. */
14
+ export declare const OLLAMA_CLOUD_ROUTE = "ollama-cloud";
15
+ /** Default credential reference naming the Ollama Cloud API key. */
16
+ export declare const DEFAULT_OLLAMA_API_KEY_REF = "OLLAMA_API_KEY";
17
+ /** Default native base URL; chat maps it to its `/v1` sibling. */
18
+ export declare const DEFAULT_OLLAMA_BASE_URL = "https://ollama.com/api";
19
+ /** Fallback per-request output cap; Ollama publishes no per-model limit. */
20
+ export declare const OLLAMA_DEFAULT_MAX_TOKENS = 32768;
21
+ /** Fallback context window for a row the endpoint never described. */
22
+ export declare const OLLAMA_DEFAULT_CONTEXT_WINDOW = 262144;
23
+ /**
24
+ * Budget for the Settings card's explicit cloud listing. Longer than the
25
+ * picker's discovery bound because a user waits on this one and `/api/show`
26
+ * runs once per model.
27
+ */
28
+ export declare const OLLAMA_PULL_TIMEOUT_MS = 20000;
29
+ /**
30
+ * The window the session plans against: the user's override, capped at what
31
+ * the endpoint advertises (or, without discovery, the stored snapshot). The
32
+ * cap keeps a stale or mistyped override from planning past the model.
33
+ */
34
+ export declare function effectiveContextWindow(row: {
35
+ contextWindow: number;
36
+ contextOverride: number | undefined;
37
+ }, discovered: number | undefined): number;
38
+ export interface OllamaAdapterOptions {
39
+ /** Resolve the API key per request; `undefined` means unconfigured. */
40
+ apiKey: () => Promise<string | undefined>;
41
+ /**
42
+ * Live native base URL (`https://ollama.com/api`); chat maps it to `/v1`.
43
+ * Read per request so a Settings save takes effect without a restart.
44
+ */
45
+ baseURL: () => string;
46
+ /**
47
+ * Live catalog snapshot, in picker order. The Settings store (or a
48
+ * non-empty config override) is the only source; an empty list means the
49
+ * route serves no model.
50
+ */
51
+ catalog: () => readonly OllamaModelSetting[];
52
+ /** Per-model default-effort override, winning over the stored row. */
53
+ defaultEffortOf?: (model: string) => string | undefined;
54
+ /** Live discovery via `/api/tags` + `/api/show` (default true). */
55
+ discovery?: boolean;
56
+ fetchFn?: FetchFn;
57
+ resolveAttachments?: () => AttachmentStore | undefined;
58
+ streamIdleTimeoutMs?: number;
59
+ onWarn?: (message: string) => void;
60
+ }
61
+ /** Map a native base URL to its OpenAI-compatible chat sibling. */
62
+ export declare function ollamaChatBaseURL(baseURL: string): string;
63
+ /**
64
+ * Normalize one listing id to the wire id the chat endpoint accepts. The
65
+ * Cloud listing advertises bare names (`glm-5.3`) while its chat wire wants
66
+ * the `:cloud` tag; a local daemon reached through a custom base URL takes
67
+ * the bare name, so only the official host gets the tag. An id that already
68
+ * carries a tag is returned unchanged.
69
+ * @param id - the id as listed by `/api/tags`.
70
+ * @param baseURL - the native base URL the listing came from.
71
+ * @returns the id to store and send on the chat wire.
72
+ */
73
+ export declare function normalizeOllamaModelId(id: string, baseURL: string): string;
74
+ export interface OllamaDiscoveryOptions {
75
+ baseURL: string;
76
+ apiKey: string | undefined;
77
+ fetchFn?: FetchFn | undefined;
78
+ signal?: AbortSignal | undefined;
79
+ }
80
+ /** One discovered Cloud model, with the capability flags the card badges. */
81
+ export interface OllamaDiscoveredModel extends DiscoveredModel {
82
+ /** The id exactly as `/api/tags` published it, before wire normalization. */
83
+ rawId: string;
84
+ /** The endpoint advertised the `thinking` capability. */
85
+ thinking: boolean;
86
+ }
87
+ /**
88
+ * List Cloud models via `/api/tags`, enriching through `/api/show`. A failed
89
+ * enrichment degrades to an id-only row; a failed listing fails. Listing ids
90
+ * are normalized to their wire form ({@link normalizeOllamaModelId}) while
91
+ * `/api/show` still receives the id the endpoint published.
92
+ */
93
+ export declare function fetchOllamaModels(options: OllamaDiscoveryOptions): Promise<OllamaDiscoveredModel[]>;
94
+ export declare function ollamaChatRequestBody(options: GenerateOptions, messages: Record<string, unknown>[], maxTokens: number | undefined): Record<string, unknown>;
95
+ export declare class OllamaAdapter extends LlmAdapter {
96
+ private readonly options;
97
+ private readonly catalogCache;
98
+ constructor(options: OllamaAdapterOptions);
99
+ /** Drop the discovery snapshot so the next picker pass re-reads the endpoint. */
100
+ invalidateModels(): void;
101
+ private rows;
102
+ providerInfo(provider: string): LlmProviderInfo;
103
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
104
+ private staticList;
105
+ resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
106
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
107
+ }