dsh-subscriptions 0.1.5 → 0.2.2

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 (110) hide show
  1. package/README.md +212 -154
  2. package/lib/auth/claude-code-creds.js +31 -9
  3. package/lib/auth/device-flow.js +16 -9
  4. package/lib/auth/oauth-flow.js +13 -10
  5. package/lib/auth/rpc.d.ts +73 -2
  6. package/lib/auth/rpc.js +124 -23
  7. package/lib/auth/store.js +80 -31
  8. package/lib/client/CursorCard.d.ts +1 -1
  9. package/lib/client/CursorCard.js +50 -16
  10. package/lib/client/ExternalUsageCards.d.ts +1 -1
  11. package/lib/client/ExternalUsageCards.js +37 -16
  12. package/lib/client/ImageGallery.d.ts +2 -2
  13. package/lib/client/ImageGallery.js +92 -33
  14. package/lib/client/ImageGenerateToolview.js +50 -14
  15. package/lib/client/OllamaCard.d.ts +13 -0
  16. package/lib/client/OllamaCard.js +94 -0
  17. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  18. package/lib/client/OllamaSettingsManager.js +329 -0
  19. package/lib/client/ProviderAccountManager.d.ts +1 -1
  20. package/lib/client/ProviderAccountManager.js +164 -49
  21. package/lib/client/ProviderModelEditor.js +109 -26
  22. package/lib/client/SpeedSelect.d.ts +62 -14
  23. package/lib/client/SpeedSelect.js +257 -46
  24. package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
  25. package/lib/client/SubscriptionUsageBadge.js +301 -112
  26. package/lib/client/SubscriptionsSection.d.ts +2 -0
  27. package/lib/client/SubscriptionsSection.js +259 -85
  28. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  29. package/lib/client/UsageMeter.d.ts +1 -1
  30. package/lib/client/UsageMeter.js +69 -27
  31. package/lib/client/VideoGenerateToolview.js +69 -17
  32. package/lib/client/account-preferences.d.ts +8 -0
  33. package/lib/client/account-preferences.js +20 -5
  34. package/lib/client/host-contract.d.ts +11 -2
  35. package/lib/client/host-contract.js +20 -3
  36. package/lib/client/index.d.ts +5 -5
  37. package/lib/client/index.js +34 -12
  38. package/lib/client/locales.d.ts +78 -0
  39. package/lib/client/locales.js +121 -34
  40. package/lib/client/ollama-draft.d.ts +229 -0
  41. package/lib/client/ollama-draft.js +255 -0
  42. package/lib/client/reset-action.js +6 -4
  43. package/lib/client/reset-credits-view.d.ts +1 -1
  44. package/lib/client/reset-credits-view.js +66 -17
  45. package/lib/client/subscription-card-styles.js +94 -24
  46. package/lib/client/usage-color-preferences.js +4 -2
  47. package/lib/client/usage-pace.d.ts +7 -0
  48. package/lib/client/usage-pace.js +43 -10
  49. package/lib/client.js +1647 -266
  50. package/lib/client.js.map +1 -1
  51. package/lib/http.js +1 -1
  52. package/lib/index.d.ts +28 -3
  53. package/lib/index.js +9329 -8217
  54. package/lib/model-defaults.js +4 -2
  55. package/lib/provider-settings.d.ts +14 -1
  56. package/lib/provider-settings.js +64 -20
  57. package/lib/providers/account-preferences.js +51 -22
  58. package/lib/providers/accounts.js +15 -9
  59. package/lib/providers/antigravity-oauth-client.js +2 -2
  60. package/lib/providers/antigravity.js +141 -68
  61. package/lib/providers/catalog-store.js +120 -41
  62. package/lib/providers/claude-cli-version.js +3 -3
  63. package/lib/providers/claude.d.ts +23 -3
  64. package/lib/providers/claude.js +167 -92
  65. package/lib/providers/codex-client-version.js +3 -3
  66. package/lib/providers/codex-search.js +40 -13
  67. package/lib/providers/codex.js +196 -112
  68. package/lib/providers/common.d.ts +14 -0
  69. package/lib/providers/common.js +118 -38
  70. package/lib/providers/copilot.js +143 -82
  71. package/lib/providers/cursor-adapter.js +42 -18
  72. package/lib/providers/cursor-auth.d.ts +1 -1
  73. package/lib/providers/cursor-auth.js +69 -24
  74. package/lib/providers/cursor-model-settings.js +14 -5
  75. package/lib/providers/cursor-usage.js +27 -13
  76. package/lib/providers/external-usage-controller.d.ts +9 -2
  77. package/lib/providers/external-usage-controller.js +30 -7
  78. package/lib/providers/external-usage.d.ts +8 -0
  79. package/lib/providers/external-usage.js +140 -20
  80. package/lib/providers/grok.js +152 -89
  81. package/lib/providers/image-pool.js +29 -11
  82. package/lib/providers/minimax-usage.js +31 -11
  83. package/lib/providers/npm-cli-version.js +10 -3
  84. package/lib/providers/ollama-settings.d.ts +90 -0
  85. package/lib/providers/ollama-settings.js +213 -0
  86. package/lib/providers/ollama-web.d.ts +46 -0
  87. package/lib/providers/ollama-web.js +231 -0
  88. package/lib/providers/ollama.d.ts +107 -0
  89. package/lib/providers/ollama.js +608 -0
  90. package/lib/providers/pool-family.js +11 -3
  91. package/lib/providers/pool-health.js +28 -8
  92. package/lib/providers/pool-usage.js +16 -9
  93. package/lib/providers/pool.d.ts +8 -0
  94. package/lib/providers/pool.js +81 -42
  95. package/lib/providers/rate-limit.js +8 -2
  96. package/lib/providers/replay.js +40 -17
  97. package/lib/providers/reset-redemption.js +18 -6
  98. package/lib/tools/image-generate.js +123 -65
  99. package/lib/tools/registration.js +5 -2
  100. package/lib/tools/video-generate.js +51 -31
  101. package/lib/tools/x-search.js +50 -22
  102. package/lib/translate/anthropic.js +85 -37
  103. package/lib/translate/antigravity-schema.js +71 -28
  104. package/lib/translate/antigravity-thinking.js +22 -9
  105. package/lib/translate/antigravity.js +141 -50
  106. package/lib/translate/chat-completions.js +67 -23
  107. package/lib/translate/resolved.js +55 -21
  108. package/lib/translate/responses.js +69 -29
  109. package/lib/translate/sse.js +4 -1
  110. package/package.json +26 -4
@@ -6,7 +6,10 @@ import { parseProviderJson } from './common.js';
6
6
  * bar.
7
7
  */
8
8
  function usedFromCounts(total, count) {
9
- return typeof total === 'number' && Number.isFinite(total) && total > 0 && typeof count === 'number'
9
+ return typeof total === 'number' &&
10
+ Number.isFinite(total) &&
11
+ total > 0 &&
12
+ typeof count === 'number'
10
13
  ? (1 - count / total) * 100
11
14
  : undefined;
12
15
  }
@@ -36,13 +39,18 @@ export async function fetchMiniMaxUsage(key, region, http = fetch, signal) {
36
39
  if (key.startsWith('sk-api-'))
37
40
  throw new Error('MiniMax requires a subscription key, not a pay-as-you-go key');
38
41
  const response = await http(`https://www.minimax.${region === 'cn' ? 'cn' : 'io'}/v1/token_plan/remains`, {
39
- headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' },
42
+ headers: {
43
+ authorization: `Bearer ${key}`,
44
+ 'content-type': 'application/json',
45
+ },
40
46
  ...(signal === undefined ? {} : { signal }),
41
47
  });
42
48
  if (!response.ok)
43
49
  throw new Error(`MiniMax usage endpoint returned HTTP ${response.status}`);
44
- const body = await parseProviderJson(response, 'MiniMax usage');
45
- if ((body?.base_resp?.status_code !== undefined && body.base_resp.status_code !== 0) || !Array.isArray(body?.model_remains)) {
50
+ const body = (await parseProviderJson(response, 'MiniMax usage'));
51
+ if ((body?.base_resp?.status_code !== undefined &&
52
+ body.base_resp.status_code !== 0) ||
53
+ !Array.isArray(body?.model_remains)) {
46
54
  throw new Error('MiniMax usage response failed or has no quota records');
47
55
  }
48
56
  const windows = [];
@@ -60,22 +68,34 @@ export async function fetchMiniMaxUsage(key, region, http = fetch, signal) {
60
68
  if (status === 3)
61
69
  continue; // Unlimited pools cannot have a finite percentage.
62
70
  const remaining = row[`${prefix}_remaining_percent`];
63
- const boost = prefix === 'current_weekly' ? row.weekly_boost_permille ?? 1000 : 1000;
71
+ const boost = prefix === 'current_weekly' ? (row.weekly_boost_permille ?? 1000) : 1000;
64
72
  if (typeof boost !== 'number' || !Number.isFinite(boost) || boost <= 0)
65
73
  continue;
66
74
  // An explicit percentage needs no counts: time-based plans send zero
67
75
  // counts beside it. Official CLI compatibility: without a percentage, the
68
76
  // legacy counts are the quota remaining.
69
- const usedPercent = typeof remaining === 'number' ? 100 - remaining * boost / 1000
77
+ const usedPercent = typeof remaining === 'number'
78
+ ? 100 - (remaining * boost) / 1000
70
79
  : usedFromCounts(row[`${prefix}_total_count`], row[`${prefix}_usage_count`]);
71
- if (usedPercent === undefined || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100)
80
+ if (usedPercent === undefined ||
81
+ !Number.isFinite(usedPercent) ||
82
+ usedPercent < 0 ||
83
+ usedPercent > 100)
72
84
  continue;
73
85
  const start = row[startKey];
74
86
  const end = row[endKey];
75
- const validBounds = typeof start === 'number' && Number.isFinite(start) && start > 0
76
- && typeof end === 'number' && Number.isFinite(end) && end > start;
77
- windows.push({ kind: fallback === 'session' && validBounds && end - start !== 18_000_000 ? 'other' : fallback,
78
- scope: row.model_name, usedPercent,
87
+ const validBounds = typeof start === 'number' &&
88
+ Number.isFinite(start) &&
89
+ start > 0 &&
90
+ typeof end === 'number' &&
91
+ Number.isFinite(end) &&
92
+ end > start;
93
+ windows.push({
94
+ kind: fallback === 'session' && validBounds && end - start !== 18_000_000
95
+ ? 'other'
96
+ : fallback,
97
+ scope: row.model_name,
98
+ usedPercent,
79
99
  ...(validBounds ? { startsAt: start, resetsAt: end } : {}),
80
100
  });
81
101
  }
@@ -34,13 +34,17 @@ export class NpmCliVersionCache {
34
34
  this.timeoutMs = options.timeoutMs ?? 5000;
35
35
  }
36
36
  /** A manual catalog refresh also checks for a newly released CLI. */
37
- invalidate() { this.expiresAt = 0; }
37
+ invalidate() {
38
+ this.expiresAt = 0;
39
+ }
38
40
  resolve() {
39
41
  if (this.pending !== undefined)
40
42
  return this.pending;
41
43
  if (this.presented !== undefined && this.now() < this.expiresAt)
42
44
  return Promise.resolve(this.presented.version);
43
- this.pending = this.refresh().finally(() => { this.pending = undefined; });
45
+ this.pending = this.refresh().finally(() => {
46
+ this.pending = undefined;
47
+ });
44
48
  return this.pending;
45
49
  }
46
50
  /**
@@ -82,7 +86,10 @@ export class NpmCliVersionCache {
82
86
  }, this.timeoutMs);
83
87
  });
84
88
  // Also bounded when an injected transport ignores cancellation.
85
- this.presented = { version: await Promise.race([lookup(), timeout]), source: 'npm' };
89
+ this.presented = {
90
+ version: await Promise.race([lookup(), timeout]),
91
+ source: 'npm',
92
+ };
86
93
  this.expiresAt = this.now() + 6 * 60 * 60_000;
87
94
  }
88
95
  catch {
@@ -0,0 +1,90 @@
1
+ /**
2
+ * One model the Ollama Cloud route serves. The route ships no catalog of its
3
+ * own: this list is the catalog, in the order the picker shows it. Adoption
4
+ * fills the capability fields from the endpoint's `/api/tags` + `/api/show`
5
+ * reply; every field except `id` is optional so a hand-written row still
6
+ * works.
7
+ */
8
+ export interface OllamaModelSetting {
9
+ /** Wire model id; must be non-empty and unique within the list. */
10
+ id: string;
11
+ /** Selector label; defaults to the id. */
12
+ name?: string;
13
+ /** Known combined request/response context capacity in tokens. */
14
+ contextWindow?: number;
15
+ /**
16
+ * The user's local context-window choice. Unlike `contextWindow`, which
17
+ * snapshots what the endpoint advertised, this records only an explicit
18
+ * edit: absent follows the endpoint. The runtime caps it at the advertised
19
+ * window and it never widens the API's real capacity.
20
+ */
21
+ contextOverride?: number;
22
+ /** Per-request output cap; defaults to the route's fallback. */
23
+ maxTokens?: number;
24
+ /** The endpoint advertised the `vision` capability. */
25
+ vision?: boolean;
26
+ /** The endpoint advertised the `thinking` capability. */
27
+ thinking?: boolean;
28
+ /** Advertised reasoning efforts, in picker order; empty means non-reasoning. */
29
+ efforts?: string[];
30
+ /** Effort preselected when the session picks none; must be one of `efforts`. */
31
+ defaultEffort?: string;
32
+ }
33
+ /**
34
+ * Durable, non-secret Ollama Cloud configuration. Every field is optional and
35
+ * its absence means "no override": the effective value then falls back to the
36
+ * plugin config and, last, to the built-in default. The API key itself never
37
+ * appears here; `apiKeyRef` only names a DSH credential.
38
+ */
39
+ export interface OllamaSettings {
40
+ /** The route's catalog, in picker order. `[]` hides every model. */
41
+ models?: OllamaModelSetting[];
42
+ /** Register the web search/fetch providers; the config value is the default. */
43
+ web?: boolean;
44
+ /** Native base URL; the config value is the default. */
45
+ baseURL?: string;
46
+ /** Credential reference naming the API key; the config value is the default. */
47
+ apiKeyRef?: string;
48
+ }
49
+ /**
50
+ * Accept only an absolute http(s) base URL with no credentials, query, or
51
+ * fragment, and return its trimmed form. The route appends `/tags`, `/show`,
52
+ * and `/v1/chat/completions` to this value, so anything else would build a URL
53
+ * that cannot work — and a non-official host receives the referenced
54
+ * credential, which the dialog tells the user.
55
+ * @param value - the candidate URL.
56
+ * @returns the trimmed URL to store.
57
+ * @throws when the value is not a usable absolute http(s) URL.
58
+ */
59
+ export declare function validateOllamaBaseURL(value: unknown): string;
60
+ /**
61
+ * Validate one settings document, dropping unknown keys and duplicate model
62
+ * ids (first wins, matching the Cursor store's visible-model handling).
63
+ * @param input - the parsed store document or an RPC payload.
64
+ * @throws when a known field has the wrong type or an unusable value.
65
+ */
66
+ export declare function validateOllamaSettings(input: unknown): OllamaSettings;
67
+ /**
68
+ * Durable Ollama Cloud configuration under the DSH home directory. Writes are
69
+ * serialized and atomic (temp file with mode 0600, then rename), so a crash
70
+ * never leaves a half-written document and a concurrent writer cannot lose an
71
+ * earlier save.
72
+ */
73
+ export declare class OllamaSettingsStore {
74
+ readonly path: string;
75
+ private readonly onCorrupt?;
76
+ private current;
77
+ private writes;
78
+ /** Set when the stored file existed but was unreadable or invalid. */
79
+ private corrupt;
80
+ constructor(path?: string, onCorrupt?: ((message: string) => void) | undefined);
81
+ /** Why the stored file is ignored, when it is. */
82
+ corruption(): string | undefined;
83
+ get(): OllamaSettings;
84
+ /** The stored catalog, or `undefined` when the store carries no override. */
85
+ models(): readonly OllamaModelSetting[] | undefined;
86
+ web(): boolean | undefined;
87
+ baseURL(): string | undefined;
88
+ apiKeyRef(): string | undefined;
89
+ set(input: unknown): Promise<void>;
90
+ }
@@ -0,0 +1,213 @@
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 = {
77
+ id: readNonEmptyString(record.id, `models[${index}].id`).trim(),
78
+ };
79
+ if (record.name !== undefined)
80
+ model.name = readNonEmptyString(record.name, `models[${index}].name`);
81
+ if (record.contextWindow !== undefined) {
82
+ model.contextWindow = readPositiveInteger(record.contextWindow, `models[${index}].contextWindow`);
83
+ }
84
+ if (record.contextOverride !== undefined) {
85
+ model.contextOverride = readPositiveInteger(record.contextOverride, `models[${index}].contextOverride`);
86
+ }
87
+ if (record.maxTokens !== undefined) {
88
+ model.maxTokens = readPositiveInteger(record.maxTokens, `models[${index}].maxTokens`);
89
+ }
90
+ if (record.vision !== undefined)
91
+ model.vision = readBoolean(record.vision, `models[${index}].vision`);
92
+ if (record.thinking !== undefined)
93
+ model.thinking = readBoolean(record.thinking, `models[${index}].thinking`);
94
+ if (record.efforts !== undefined)
95
+ model.efforts = readEfforts(record.efforts, `models[${index}].efforts`);
96
+ if (record.defaultEffort !== undefined) {
97
+ const effort = readNonEmptyString(record.defaultEffort, `models[${index}].defaultEffort`);
98
+ // The runtime rejects a default the model does not advertise, so a
99
+ // mismatch is a store bug rather than something to carry forward.
100
+ if (model.efforts !== undefined && !model.efforts.includes(effort)) {
101
+ fail(`models[${index}].defaultEffort must be one of its efforts`);
102
+ }
103
+ model.defaultEffort = effort;
104
+ }
105
+ return model;
106
+ }
107
+ /**
108
+ * Validate one settings document, dropping unknown keys and duplicate model
109
+ * ids (first wins, matching the Cursor store's visible-model handling).
110
+ * @param input - the parsed store document or an RPC payload.
111
+ * @throws when a known field has the wrong type or an unusable value.
112
+ */
113
+ export function validateOllamaSettings(input) {
114
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
115
+ fail('Ollama settings must be an object');
116
+ }
117
+ const record = input;
118
+ const settings = {};
119
+ if (record.models !== undefined) {
120
+ if (!Array.isArray(record.models))
121
+ fail('models must be an array');
122
+ const seen = new Set();
123
+ const models = [];
124
+ for (const [index, entry] of record.models.entries()) {
125
+ const model = readModel(entry, index);
126
+ if (seen.has(model.id))
127
+ continue;
128
+ seen.add(model.id);
129
+ models.push(model);
130
+ }
131
+ settings.models = models;
132
+ }
133
+ if (record.web !== undefined)
134
+ settings.web = readBoolean(record.web, 'web');
135
+ if (record.baseURL !== undefined)
136
+ settings.baseURL = validateOllamaBaseURL(record.baseURL);
137
+ if (record.apiKeyRef !== undefined)
138
+ settings.apiKeyRef = readApiKeyRef(record.apiKeyRef);
139
+ return settings;
140
+ }
141
+ /**
142
+ * Durable Ollama Cloud configuration under the DSH home directory. Writes are
143
+ * serialized and atomic (temp file with mode 0600, then rename), so a crash
144
+ * never leaves a half-written document and a concurrent writer cannot lose an
145
+ * earlier save.
146
+ */
147
+ export class OllamaSettingsStore {
148
+ path;
149
+ onCorrupt;
150
+ current = {};
151
+ writes = Promise.resolve();
152
+ /** Set when the stored file existed but was unreadable or invalid. */
153
+ corrupt;
154
+ constructor(path = dshHomePath('plugins', 'subscriptions', 'ollama-settings.json'), onCorrupt) {
155
+ this.path = path;
156
+ this.onCorrupt = onCorrupt;
157
+ try {
158
+ this.current = validateOllamaSettings(JSON.parse(readFileSync(path, 'utf8')));
159
+ }
160
+ catch (error) {
161
+ if (error.code !== 'ENOENT') {
162
+ // A broken file must not take the whole plugin down: degrade to
163
+ // defaults, keep the file untouched for manual recovery, and let
164
+ // the next save overwrite it.
165
+ this.corrupt = error instanceof Error ? error.message : String(error);
166
+ // The raw V8 message quotes the offending bytes; the log keeps a
167
+ // fixed diagnosis and corruption() carries the detail for callers
168
+ // that surface it outside log lines.
169
+ this.onCorrupt?.(`Ollama settings at ${path} are unreadable and ignored; defaults serve until the next save.`);
170
+ }
171
+ }
172
+ }
173
+ /** Why the stored file is ignored, when it is. */
174
+ corruption() {
175
+ return this.corrupt;
176
+ }
177
+ get() {
178
+ return structuredClone(this.current);
179
+ }
180
+ /** The stored catalog, or `undefined` when the store carries no override. */
181
+ models() {
182
+ return this.current.models;
183
+ }
184
+ web() {
185
+ return this.current.web;
186
+ }
187
+ baseURL() {
188
+ return this.current.baseURL;
189
+ }
190
+ apiKeyRef() {
191
+ return this.current.apiKeyRef;
192
+ }
193
+ set(input) {
194
+ const validated = validateOllamaSettings(input);
195
+ const run = this.writes.then(async () => {
196
+ await mkdir(dirname(this.path), { recursive: true });
197
+ const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
198
+ try {
199
+ await writeFile(temporary, JSON.stringify(validated, null, 2), {
200
+ mode: 0o600,
201
+ });
202
+ await rename(temporary, this.path);
203
+ }
204
+ finally {
205
+ await rm(temporary, { force: true });
206
+ }
207
+ this.current = validated;
208
+ this.corrupt = undefined;
209
+ });
210
+ this.writes = run.catch(() => undefined);
211
+ return run;
212
+ }
213
+ }
@@ -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
+ }