dsh-subscriptions 0.2.0 → 0.2.3

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 (105) hide show
  1. package/README.md +220 -180
  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 +8 -4
  6. package/lib/auth/rpc.js +89 -25
  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 +1 -1
  16. package/lib/client/OllamaCard.js +26 -8
  17. package/lib/client/OllamaSettingsManager.js +113 -30
  18. package/lib/client/ProviderAccountManager.d.ts +1 -1
  19. package/lib/client/ProviderAccountManager.js +164 -49
  20. package/lib/client/ProviderModelEditor.js +137 -34
  21. package/lib/client/SpeedSelect.d.ts +62 -14
  22. package/lib/client/SpeedSelect.js +261 -49
  23. package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
  24. package/lib/client/SubscriptionUsageBadge.js +300 -112
  25. package/lib/client/SubscriptionsSection.d.ts +2 -0
  26. package/lib/client/SubscriptionsSection.js +265 -85
  27. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  28. package/lib/client/UsageMeter.d.ts +1 -1
  29. package/lib/client/UsageMeter.js +62 -24
  30. package/lib/client/VideoGenerateToolview.js +69 -17
  31. package/lib/client/account-preferences.d.ts +8 -0
  32. package/lib/client/account-preferences.js +20 -5
  33. package/lib/client/host-contract.d.ts +11 -2
  34. package/lib/client/host-contract.js +20 -3
  35. package/lib/client/index.d.ts +5 -5
  36. package/lib/client/index.js +34 -12
  37. package/lib/client/locales.d.ts +22 -0
  38. package/lib/client/locales.js +73 -51
  39. package/lib/client/model-filter.d.ts +37 -0
  40. package/lib/client/model-filter.js +45 -0
  41. package/lib/client/ollama-draft.js +21 -12
  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.js +30 -12
  48. package/lib/client/usage-window-filter.d.ts +7 -0
  49. package/lib/client/usage-window-filter.js +12 -0
  50. package/lib/client.js +480 -136
  51. package/lib/client.js.map +1 -1
  52. package/lib/http.js +1 -1
  53. package/lib/index.d.ts +3 -3
  54. package/lib/index.js +167 -76
  55. package/lib/model-defaults.js +4 -2
  56. package/lib/provider-settings.d.ts +17 -0
  57. package/lib/provider-settings.js +79 -16
  58. package/lib/providers/account-preferences.js +51 -22
  59. package/lib/providers/accounts.js +15 -9
  60. package/lib/providers/antigravity-oauth-client.js +2 -2
  61. package/lib/providers/antigravity.js +141 -68
  62. package/lib/providers/catalog-store.js +120 -41
  63. package/lib/providers/claude-cli-version.js +3 -3
  64. package/lib/providers/claude.d.ts +9 -2
  65. package/lib/providers/claude.js +153 -90
  66. package/lib/providers/codex-client-version.js +3 -3
  67. package/lib/providers/codex-search.js +40 -13
  68. package/lib/providers/codex.js +196 -112
  69. package/lib/providers/common.js +71 -39
  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.js +14 -5
  77. package/lib/providers/external-usage.js +59 -23
  78. package/lib/providers/grok.js +152 -89
  79. package/lib/providers/image-pool.js +29 -11
  80. package/lib/providers/minimax-usage.js +31 -11
  81. package/lib/providers/npm-cli-version.js +10 -3
  82. package/lib/providers/ollama-settings.js +24 -8
  83. package/lib/providers/ollama-web.js +29 -11
  84. package/lib/providers/ollama.js +129 -66
  85. package/lib/providers/pool-family.js +11 -3
  86. package/lib/providers/pool-health.js +28 -8
  87. package/lib/providers/pool-usage.js +16 -9
  88. package/lib/providers/pool.d.ts +8 -0
  89. package/lib/providers/pool.js +81 -42
  90. package/lib/providers/rate-limit.js +8 -2
  91. package/lib/providers/replay.js +40 -17
  92. package/lib/providers/reset-redemption.js +18 -6
  93. package/lib/tools/image-generate.js +123 -65
  94. package/lib/tools/registration.js +5 -2
  95. package/lib/tools/video-generate.js +51 -31
  96. package/lib/tools/x-search.js +50 -22
  97. package/lib/translate/anthropic.js +85 -37
  98. package/lib/translate/antigravity-schema.js +71 -28
  99. package/lib/translate/antigravity-thinking.js +22 -9
  100. package/lib/translate/antigravity.js +151 -52
  101. package/lib/translate/chat-completions.js +67 -23
  102. package/lib/translate/resolved.js +55 -21
  103. package/lib/translate/responses.js +69 -29
  104. package/lib/translate/sse.js +4 -1
  105. package/package.json +27 -4
@@ -9,7 +9,7 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
9
9
  import { DISCOVERY_TIMEOUT_MS, effortDisplayName, httpLlmError, idleWatchdog, isDiscoveryAborted, isMissingOrInvalidCredential, mapFetchFailure, ModelCatalogCache, readBoundedJson, withTimeout, } from './common.js';
10
10
  import { hostFetch } from '../http.js';
11
11
  import { resolveImages } from '../translate/resolved.js';
12
- import { streamChatCompletions, toChatMessages, toChatTools } from '../translate/chat-completions.js';
12
+ import { streamChatCompletions, toChatMessages, toChatTools, } from '../translate/chat-completions.js';
13
13
  /** Route id owned by this adapter. Not a member of the OAuth `PROVIDER_IDS`. */
14
14
  export const OLLAMA_CLOUD_ROUTE = 'ollama-cloud';
15
15
  /** Default credential reference naming the Ollama Cloud API key. */
@@ -43,11 +43,15 @@ const OLLAMA_REASONING_FAMILIES = [
43
43
  // Recorded before the endpoint's level list was read: the thinking values
44
44
  // ride as `off`/`low`/`high`/`max` (`off` maps to the wire spelling
45
45
  // `none`), and the vendor publishes no `medium` level.
46
- { prefix: 'deepseek-v4.1-flash', efforts: ['off', 'low', 'high', 'max'], defaultEffort: 'high' },
46
+ {
47
+ prefix: 'deepseek-v4.1-flash',
48
+ efforts: ['off', 'low', 'high', 'max'],
49
+ defaultEffort: 'high',
50
+ },
47
51
  { prefix: 'glm-5.3', efforts: ['low', 'high', 'max'], defaultEffort: 'max' },
48
52
  ];
49
53
  function reasoningFamily(id) {
50
- return OLLAMA_REASONING_FAMILIES.find(family => id.startsWith(family.prefix));
54
+ return OLLAMA_REASONING_FAMILIES.find((family) => id.startsWith(family.prefix));
51
55
  }
52
56
  /**
53
57
  * The window the session plans against: the user's override, capped at what
@@ -93,16 +97,17 @@ export function normalizeOllamaModelId(id, baseURL) {
93
97
  * 403 may be a transient scope denial, so it falls back to the stored
94
98
  * catalog like any other discovery miss. */
95
99
  function isUnauthorized(error) {
96
- return error instanceof LlmError && error.code === 'AUTH'
97
- && error.failure?.status === 401;
100
+ return (error instanceof LlmError &&
101
+ error.code === 'AUTH' &&
102
+ error.failure?.status === 401);
98
103
  }
99
104
  /**
100
105
  * Find a discovered row for a catalog id, tolerating a listing endpoint that
101
106
  * still reports bare names (`glm-5.3` for catalogued `glm-5.3:cloud`).
102
107
  */
103
108
  function discoveredFor(catalogId, discovered) {
104
- return discovered?.find(row => row.id === catalogId)
105
- ?? discovered?.find(row => row.id === catalogId.replace(/:cloud$/, ''));
109
+ return (discovered?.find((row) => row.id === catalogId) ??
110
+ discovered?.find((row) => row.id === catalogId.replace(/:cloud$/, '')));
106
111
  }
107
112
  function reasoningFor(row, configuredDefault) {
108
113
  const family = reasoningFamily(row.id);
@@ -113,7 +118,10 @@ function reasoningFor(row, configuredDefault) {
113
118
  return undefined;
114
119
  // Every level here comes from the row or a family that named it; a
115
120
  // stored default the row does not list cannot ride the wire.
116
- const efforts = row.efforts.map(effort => ({ id: ReasoningEffortId(effort), name: effortDisplayName(effort) }));
121
+ const efforts = row.efforts.map((effort) => ({
122
+ id: ReasoningEffortId(effort),
123
+ name: effortDisplayName(effort),
124
+ }));
117
125
  const storedDefault = row.efforts.includes(row.defaultEffort ?? '')
118
126
  ? row.defaultEffort
119
127
  : undefined;
@@ -124,10 +132,12 @@ function reasoningFor(row, configuredDefault) {
124
132
  familyDefault,
125
133
  storedDefault,
126
134
  ];
127
- const defaultEffort = priority.find(entry => entry !== undefined && row.efforts.includes(entry));
135
+ const defaultEffort = priority.find((entry) => entry !== undefined && row.efforts.includes(entry));
128
136
  return {
129
137
  efforts,
130
- ...defaultEffort === undefined ? {} : { defaultEffort: ReasoningEffortId(defaultEffort) },
138
+ ...(defaultEffort === undefined
139
+ ? {}
140
+ : { defaultEffort: ReasoningEffortId(defaultEffort) }),
131
141
  };
132
142
  }
133
143
  /**
@@ -143,14 +153,21 @@ function reasoningFor(row, configuredDefault) {
143
153
  * @returns the merged block, with a default that is always one of its levels.
144
154
  */
145
155
  function mergeDiscoveredReasoning(row, discovered, configuredDefault) {
146
- const vendorLevels = discovered.efforts.map(entry => String(entry.id));
156
+ const vendorLevels = discovered.efforts.map((entry) => String(entry.id));
147
157
  // A published vendor set is closed: the stored row and the family table
148
158
  // must not widen it. When the endpoint names no levels, the stored row
149
159
  // supplies them and only a still-empty list falls to the family table.
150
160
  const efforts = vendorLevels.length > 0
151
161
  ? vendorLevels
152
- : [...row.efforts, ...(row.efforts.length > 0 ? [] : reasoningFamily(row.id)?.efforts ?? [])];
153
- const vendorDefault = discovered.defaultEffort === undefined ? undefined : String(discovered.defaultEffort);
162
+ : [
163
+ ...row.efforts,
164
+ ...(row.efforts.length > 0
165
+ ? []
166
+ : (reasoningFamily(row.id)?.efforts ?? [])),
167
+ ];
168
+ const vendorDefault = discovered.defaultEffort === undefined
169
+ ? undefined
170
+ : String(discovered.defaultEffort);
154
171
  // The user's explicit choice wins only within the levels the model still
155
172
  // offers: a stored default outside a retracted vendor set must not ride
156
173
  // the wire, so it falls through to the vendor or stored fallback.
@@ -158,28 +175,33 @@ function mergeDiscoveredReasoning(row, discovered, configuredDefault) {
158
175
  configuredDefault,
159
176
  vendorDefault,
160
177
  // The family default is the last resort, not a user preference.
161
- ...configuredDefault !== undefined || vendorDefault !== undefined
178
+ ...(configuredDefault !== undefined || vendorDefault !== undefined
162
179
  ? [undefined, reasoningFamily(row.id)?.defaultEffort]
163
- : [row.defaultEffort, reasoningFamily(row.id)?.defaultEffort],
180
+ : [row.defaultEffort, reasoningFamily(row.id)?.defaultEffort]),
164
181
  ];
165
- const defaultEffort = priority.find(entry => entry !== undefined && efforts.includes(entry));
182
+ const defaultEffort = priority.find((entry) => entry !== undefined && efforts.includes(entry));
166
183
  return {
167
- efforts: efforts.map(effort => ({ id: ReasoningEffortId(effort), name: effortDisplayName(effort) })),
168
- ...defaultEffort === undefined ? {} : { defaultEffort: ReasoningEffortId(defaultEffort) },
184
+ efforts: efforts.map((effort) => ({
185
+ id: ReasoningEffortId(effort),
186
+ name: effortDisplayName(effort),
187
+ })),
188
+ ...(defaultEffort === undefined
189
+ ? {}
190
+ : { defaultEffort: ReasoningEffortId(defaultEffort) }),
169
191
  };
170
192
  }
171
193
  /** Strip image blocks for models that cannot accept vision input. Runs before
172
194
  * image resolution so a text-only model never requires the attachment store. */
173
195
  function withoutImages(messages) {
174
- return messages.map(message => ({
196
+ return messages.map((message) => ({
175
197
  ...message,
176
- content: message.content.filter(block => block.type !== 'image'),
198
+ content: message.content.filter((block) => block.type !== 'image'),
177
199
  }));
178
200
  }
179
201
  function discoveryHeaders(apiKey) {
180
202
  return {
181
- 'accept': 'application/json',
182
- ...apiKey === undefined ? {} : { 'authorization': `Bearer ${apiKey}` },
203
+ accept: 'application/json',
204
+ ...(apiKey === undefined ? {} : { authorization: `Bearer ${apiKey}` }),
183
205
  ...attributionHeaders(),
184
206
  };
185
207
  }
@@ -205,8 +227,10 @@ function extractContextWindow(show) {
205
227
  const info = recordOf(show['model_info']);
206
228
  if (info !== undefined) {
207
229
  for (const [key, value] of Object.entries(info)) {
208
- if (key.endsWith('context_length') && typeof value === 'number'
209
- && Number.isSafeInteger(value) && value > 0)
230
+ if (key.endsWith('context_length') &&
231
+ typeof value === 'number' &&
232
+ Number.isSafeInteger(value) &&
233
+ value > 0)
210
234
  return value;
211
235
  }
212
236
  }
@@ -228,7 +252,11 @@ function extractThinking(show) {
228
252
  const values = [];
229
253
  if (Array.isArray(rawValues)) {
230
254
  for (const entry of rawValues) {
231
- const effort = entry === false ? 'off' : typeof entry === 'string' && entry.length > 0 ? entry : undefined;
255
+ const effort = entry === false
256
+ ? 'off'
257
+ : typeof entry === 'string' && entry.length > 0
258
+ ? entry
259
+ : undefined;
232
260
  if (effort !== undefined && !values.includes(effort))
233
261
  values.push(effort);
234
262
  }
@@ -236,7 +264,9 @@ function extractThinking(show) {
236
264
  const rawDefault = thinking['default'];
237
265
  return {
238
266
  values,
239
- ...typeof rawDefault === 'string' && rawDefault.length > 0 ? { default: rawDefault } : {},
267
+ ...(typeof rawDefault === 'string' && rawDefault.length > 0
268
+ ? { default: rawDefault }
269
+ : {}),
240
270
  };
241
271
  }
242
272
  function discoveredFromShow(id, rawId, show) {
@@ -249,8 +279,12 @@ function discoveredFromShow(id, rawId, show) {
249
279
  const vendor = thinking ? extractThinking(show) : undefined;
250
280
  // The vendor's level list is the truth about the live model; the family
251
281
  // table only answers a model whose thinking capability came without one.
252
- const family = thinking && (vendor === undefined || vendor.values.length === 0) ? reasoningFamily(id) : undefined;
253
- const efforts = vendor !== undefined && vendor.values.length > 0 ? vendor.values : family?.efforts ?? [];
282
+ const family = thinking && (vendor === undefined || vendor.values.length === 0)
283
+ ? reasoningFamily(id)
284
+ : undefined;
285
+ const efforts = vendor !== undefined && vendor.values.length > 0
286
+ ? vendor.values
287
+ : (family?.efforts ?? []);
254
288
  const defaultEffort = vendor?.default ?? family?.defaultEffort;
255
289
  const contextWindow = extractContextWindow(show);
256
290
  return {
@@ -258,14 +292,21 @@ function discoveredFromShow(id, rawId, show) {
258
292
  rawId,
259
293
  name: id,
260
294
  thinking,
261
- ...contextWindow === undefined ? {} : { contextWindow },
295
+ ...(contextWindow === undefined ? {} : { contextWindow }),
262
296
  inputModalities: vision ? ['text', 'image'] : ['text'],
263
- ...efforts.length === 0 ? {} : {
264
- reasoning: {
265
- efforts: efforts.map(effort => ({ id: ReasoningEffortId(effort), name: effortDisplayName(effort) })),
266
- ...defaultEffort === undefined ? {} : { defaultEffort: ReasoningEffortId(defaultEffort) },
267
- },
268
- },
297
+ ...(efforts.length === 0
298
+ ? {}
299
+ : {
300
+ reasoning: {
301
+ efforts: efforts.map((effort) => ({
302
+ id: ReasoningEffortId(effort),
303
+ name: effortDisplayName(effort),
304
+ })),
305
+ ...(defaultEffort === undefined
306
+ ? {}
307
+ : { defaultEffort: ReasoningEffortId(defaultEffort) }),
308
+ },
309
+ }),
269
310
  };
270
311
  }
271
312
  /**
@@ -284,7 +325,7 @@ export async function fetchOllamaModels(options) {
284
325
  try {
285
326
  tagsResponse = await fetchFn(`${base}/tags`, {
286
327
  headers: discoveryHeaders(options.apiKey),
287
- ...options.signal === undefined ? {} : { signal: options.signal },
328
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
288
329
  });
289
330
  transportError = undefined;
290
331
  break;
@@ -307,7 +348,11 @@ export async function fetchOllamaModels(options) {
307
348
  const record = recordOf(entry);
308
349
  const raw = record === undefined
309
350
  ? undefined
310
- : typeof record['model'] === 'string' ? record['model'] : typeof record['name'] === 'string' ? record['name'] : undefined;
351
+ : typeof record['model'] === 'string'
352
+ ? record['model']
353
+ : typeof record['name'] === 'string'
354
+ ? record['name']
355
+ : undefined;
311
356
  if (raw === undefined || raw.length === 0)
312
357
  continue;
313
358
  const wire = normalizeOllamaModelId(raw, options.baseURL);
@@ -330,16 +375,20 @@ export async function fetchOllamaModels(options) {
330
375
  try {
331
376
  const showResponse = await fetchFn(`${base}/show`, {
332
377
  method: 'POST',
333
- headers: { 'content-type': 'application/json', ...discoveryHeaders(options.apiKey) },
378
+ headers: {
379
+ 'content-type': 'application/json',
380
+ ...discoveryHeaders(options.apiKey),
381
+ },
334
382
  body: JSON.stringify({ model: raw }),
335
- ...options.signal === undefined ? {} : { signal: options.signal },
383
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
336
384
  });
337
385
  if (!showResponse.ok)
338
386
  throw await httpLlmError(showResponse, 'ollama model details');
339
387
  const show = recordOf(await readBoundedJson(showResponse, 'ollama model details', MAX_DISCOVERY_BYTES));
340
- out[index] = show === undefined
341
- ? { id: wire, rawId: raw, name: wire, thinking: false }
342
- : discoveredFromShow(wire, raw, show);
388
+ out[index] =
389
+ show === undefined
390
+ ? { id: wire, rawId: raw, name: wire, thinking: false }
391
+ : discoveredFromShow(wire, raw, show);
343
392
  }
344
393
  catch {
345
394
  // One model's enrichment never sinks the listing.
@@ -354,17 +403,21 @@ export function ollamaChatRequestBody(options, messages, maxTokens) {
354
403
  return {
355
404
  model: options.model,
356
405
  messages,
357
- ...options.tools !== undefined && options.tools.length > 0
406
+ ...(options.tools !== undefined && options.tools.length > 0
358
407
  ? { tools: toChatTools(options.tools), tool_choice: 'auto' }
359
- : {},
408
+ : {}),
360
409
  // Ollama takes `max_tokens`. The catalog row supplies the default
361
410
  // (32,768, matching upstream); a row without one rides without it.
362
- ...maxTokens !== undefined ? { max_tokens: maxTokens } : {},
411
+ ...(maxTokens !== undefined ? { max_tokens: maxTokens } : {}),
363
412
  // `off` rides as `none` (upstream parity); any other effort rides
364
413
  // verbatim. Only advertised efforts ever reach this branch.
365
- ...options.reasoningEffort !== undefined
366
- ? { reasoning_effort: String(options.reasoningEffort) === 'off' ? 'none' : String(options.reasoningEffort) }
367
- : {},
414
+ ...(options.reasoningEffort !== undefined
415
+ ? {
416
+ reasoning_effort: String(options.reasoningEffort) === 'off'
417
+ ? 'none'
418
+ : String(options.reasoningEffort),
419
+ }
420
+ : {}),
368
421
  stream: true,
369
422
  stream_options: { include_usage: true },
370
423
  };
@@ -377,9 +430,11 @@ export class OllamaAdapter extends LlmAdapter {
377
430
  this.options = options;
378
431
  }
379
432
  /** Drop the discovery snapshot so the next picker pass re-reads the endpoint. */
380
- invalidateModels() { this.catalogCache.invalidate(); }
433
+ invalidateModels() {
434
+ this.catalogCache.invalidate();
435
+ }
381
436
  rows() {
382
- return this.options.catalog().map(entry => {
437
+ return this.options.catalog().map((entry) => {
383
438
  const family = reasoningFamily(entry.id);
384
439
  return {
385
440
  id: entry.id,
@@ -390,7 +445,7 @@ export class OllamaAdapter extends LlmAdapter {
390
445
  vision: entry.vision === true,
391
446
  efforts: entry.efforts !== undefined && entry.efforts.length > 0
392
447
  ? [...entry.efforts]
393
- : [...family?.efforts ?? []],
448
+ : [...(family?.efforts ?? [])],
394
449
  // The family default stays out of the stored row: the resolver's
395
450
  // priority keeps it last, so a vendor change is not held back by
396
451
  // the fallback baked in during construction.
@@ -416,7 +471,7 @@ export class OllamaAdapter extends LlmAdapter {
416
471
  if (this.options.discovery === false)
417
472
  return this.staticList(provider, catalog);
418
473
  try {
419
- const discovered = await withTimeout(signal => this.catalogCache.get(() => fetchOllamaModels({
474
+ const discovered = await withTimeout((signal) => this.catalogCache.get(() => fetchOllamaModels({
420
475
  baseURL: this.options.baseURL(),
421
476
  apiKey,
422
477
  fetchFn: this.options.fetchFn,
@@ -429,7 +484,7 @@ export class OllamaAdapter extends LlmAdapter {
429
484
  // Discovery enriches the stored allowlist but never extends it: the
430
485
  // user adopted an exact catalog, so unmatched discoveries stay out of
431
486
  // the picker.
432
- return catalog.map(entry => {
487
+ return catalog.map((entry) => {
433
488
  const match = discoveredFor(entry.id, discovered);
434
489
  if (match === undefined) {
435
490
  return {
@@ -446,7 +501,8 @@ export class OllamaAdapter extends LlmAdapter {
446
501
  provider,
447
502
  id: entry.id,
448
503
  name: entry.name,
449
- inputModalities: match.inputModalities ?? (entry.vision ? ['text', 'image'] : ['text']),
504
+ inputModalities: match.inputModalities ??
505
+ (entry.vision ? ['text', 'image'] : ['text']),
450
506
  };
451
507
  });
452
508
  }
@@ -462,7 +518,7 @@ export class OllamaAdapter extends LlmAdapter {
462
518
  }
463
519
  }
464
520
  staticList(provider, catalog) {
465
- return catalog.map(entry => ({
521
+ return catalog.map((entry) => ({
466
522
  provider,
467
523
  id: entry.id,
468
524
  name: entry.name,
@@ -470,7 +526,7 @@ export class OllamaAdapter extends LlmAdapter {
470
526
  }));
471
527
  }
472
528
  async resolveModel(provider, model) {
473
- const entry = this.rows().find(row => row.id === model);
529
+ const entry = this.rows().find((row) => row.id === model);
474
530
  // The pass-through native behavior is deliberately absent: an
475
531
  // uncatalogued id fails here instead of 404ing at the gateway.
476
532
  if (entry === undefined)
@@ -481,13 +537,14 @@ export class OllamaAdapter extends LlmAdapter {
481
537
  const discoveryKey = this.options.discovery === false ? undefined : await this.options.apiKey();
482
538
  const discovered = this.options.discovery === false
483
539
  ? undefined
484
- : await withTimeout(signal => this.catalogCache.resolve(() => fetchOllamaModels({
540
+ : await withTimeout((signal) => this.catalogCache.resolve(() => fetchOllamaModels({
485
541
  baseURL: this.options.baseURL(),
486
542
  apiKey: discoveryKey,
487
543
  fetchFn: this.options.fetchFn,
488
544
  signal,
489
- })), DISCOVERY_TIMEOUT_MS).then(models => discoveredFor(model, models ?? undefined), () => undefined);
490
- const modalities = discovered?.inputModalities ?? (entry.vision ? ['text', 'image'] : ['text']);
545
+ })), DISCOVERY_TIMEOUT_MS).then((models) => discoveredFor(model, models ?? undefined), () => undefined);
546
+ const modalities = discovered?.inputModalities ??
547
+ (entry.vision ? ['text', 'image'] : ['text']);
491
548
  const configuredDefault = this.options.defaultEffortOf?.(model) ?? entry.defaultEffort;
492
549
  const reasoning = discovered?.reasoning === undefined
493
550
  ? reasoningFor(entry, configuredDefault)
@@ -497,9 +554,11 @@ export class OllamaAdapter extends LlmAdapter {
497
554
  id: model,
498
555
  name: entry.name,
499
556
  inputModalities: [...modalities],
500
- context: { contextWindow: effectiveContextWindow(entry, discovered?.contextWindow) },
557
+ context: {
558
+ contextWindow: effectiveContextWindow(entry, discovered?.contextWindow),
559
+ },
501
560
  defaultMaxTokens: entry.maxTokens,
502
- ...reasoning === undefined ? {} : { reasoning },
561
+ ...(reasoning === undefined ? {} : { reasoning }),
503
562
  };
504
563
  }
505
564
  async *stream(options) {
@@ -508,10 +567,12 @@ export class OllamaAdapter extends LlmAdapter {
508
567
  const apiKey = await this.options.apiKey();
509
568
  if (apiKey === undefined)
510
569
  throw new LlmError('ollama-cloud API key is not configured', 'MISSING_CREDENTIAL');
511
- const entry = this.rows().find(row => row.id === options.model);
570
+ const entry = this.rows().find((row) => row.id === options.model);
512
571
  if (entry === undefined)
513
572
  throw new LlmError(`ollama-cloud has no configured model "${options.model}"`, 'UNKNOWN_MODEL');
514
- const requestMessages = entry.vision ? options.messages : withoutImages(options.messages);
573
+ const requestMessages = entry.vision
574
+ ? options.messages
575
+ : withoutImages(options.messages);
515
576
  const resolved = await resolveImages(requestMessages, this.options.resolveAttachments?.(), watchdog.signal);
516
577
  const body = ollamaChatRequestBody(options, toChatMessages(resolved, options.system), options.maxTokens ?? entry.maxTokens);
517
578
  const chatBase = ollamaChatBaseURL(this.options.baseURL());
@@ -519,8 +580,8 @@ export class OllamaAdapter extends LlmAdapter {
519
580
  const response = await fetchFn(`${chatBase}/chat/completions`, {
520
581
  method: 'POST',
521
582
  headers: {
522
- 'authorization': `Bearer ${apiKey}`,
523
- 'accept': 'text/event-stream',
583
+ authorization: `Bearer ${apiKey}`,
584
+ accept: 'text/event-stream',
524
585
  'content-type': 'application/json',
525
586
  ...attributionHeaders(),
526
587
  },
@@ -532,7 +593,9 @@ export class OllamaAdapter extends LlmAdapter {
532
593
  if (response.body === null) {
533
594
  throw new LlmError('ollama-cloud API returned no response body', EMPTY_RESPONSE_CODE);
534
595
  }
535
- const pulse = () => { watchdog.pulse(); };
596
+ const pulse = () => {
597
+ watchdog.pulse();
598
+ };
536
599
  yield* streamChatCompletions(response.body, pulse);
537
600
  }
538
601
  catch (error) {
@@ -30,14 +30,22 @@ export function buildAccountPools(sources) {
30
30
  entry = { members: [], info: model };
31
31
  byModel.set(model.id, entry);
32
32
  }
33
- entry.members.push({ provider, account: catalog.account, model: model.id });
33
+ entry.members.push({
34
+ provider,
35
+ account: catalog.account,
36
+ model: model.id,
37
+ });
34
38
  }
35
39
  }
36
40
  for (const [id, { members, info }] of byModel) {
37
41
  pools.set(poolKey(provider, id), {
38
42
  members,
39
- ...info.name === undefined || info.name === id ? {} : { name: info.name },
40
- ...info.description === undefined ? {} : { description: info.description },
43
+ ...(info.name === undefined || info.name === id
44
+ ? {}
45
+ : { name: info.name }),
46
+ ...(info.description === undefined
47
+ ? {}
48
+ : { description: info.description }),
41
49
  });
42
50
  }
43
51
  }
@@ -9,7 +9,7 @@
9
9
  * cooldown (transport blips say nothing about the account), or rethrow
10
10
  * (the request itself is at fault and another account would fail alike).
11
11
  */
12
- import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, QUOTA_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
12
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
13
13
  /** Registry key for one pool member. */
14
14
  export function memberKey(provider, account, model) {
15
15
  return `${provider}/${account}/${model}`;
@@ -33,7 +33,10 @@ export const TRANSIENT_COOLDOWN_MS = 60_000;
33
33
  * wall means its siblings on the SAME account would too, so the cooldown
34
34
  * parks the account (other accounts of the provider are unaffected).
35
35
  */
36
- const MODEL_SCOPED_QUOTA_PROVIDERS = new Set(['claude', 'antigravity']);
36
+ const MODEL_SCOPED_QUOTA_PROVIDERS = new Set([
37
+ 'claude',
38
+ 'antigravity',
39
+ ]);
37
40
  /** The `retry-after` an adapter propagated through `httpLlmError`, when any. */
38
41
  function retryAfterMs(error) {
39
42
  return error.failure.providerRetryAfterMs;
@@ -61,16 +64,28 @@ export function classifyPoolFailure(error, provider) {
61
64
  action: 'switch',
62
65
  cooldownMs: retryAfterMs(error) ?? DEFAULT_QUOTA_COOLDOWN_MS,
63
66
  reason: error.code,
64
- scope: MODEL_SCOPED_QUOTA_PROVIDERS.has(provider) ? 'member' : 'account',
67
+ scope: MODEL_SCOPED_QUOTA_PROVIDERS.has(provider)
68
+ ? 'member'
69
+ : 'account',
65
70
  };
66
71
  case 'AUTH':
67
72
  case 'INVALID_CREDENTIAL':
68
73
  case 'MISSING_CREDENTIAL':
69
- return { action: 'switch', cooldownMs: AUTH_COOLDOWN_MS, reason: error.code, scope: 'account' };
74
+ return {
75
+ action: 'switch',
76
+ cooldownMs: AUTH_COOLDOWN_MS,
77
+ reason: error.code,
78
+ scope: 'account',
79
+ };
70
80
  case 'SERVER':
71
81
  case 'TIMEOUT':
72
82
  case 'EMPTY_RESPONSE':
73
- return { action: 'switch', cooldownMs: TRANSIENT_COOLDOWN_MS, reason: error.code, scope: 'member' };
83
+ return {
84
+ action: 'switch',
85
+ cooldownMs: TRANSIENT_COOLDOWN_MS,
86
+ reason: error.code,
87
+ scope: 'member',
88
+ };
74
89
  case 'TRANSPORT':
75
90
  return { action: 'switch' };
76
91
  case 'HTTP_402':
@@ -78,7 +93,12 @@ export function classifyPoolFailure(error, provider) {
78
93
  // Plan/model availability is account-shaped: another account of the
79
94
  // same subscription may still serve. A 400 that is not a context-window
80
95
  // error stays HTTP_400 and throws (the request itself is at fault).
81
- return { action: 'switch', cooldownMs: TRANSIENT_COOLDOWN_MS, reason: error.code, scope: 'member' };
96
+ return {
97
+ action: 'switch',
98
+ cooldownMs: TRANSIENT_COOLDOWN_MS,
99
+ reason: error.code,
100
+ scope: 'member',
101
+ };
82
102
  case CONTEXT_WINDOW_EXCEEDED_CODE:
83
103
  case 'ABORTED':
84
104
  default:
@@ -94,8 +114,8 @@ export class PoolHealthRegistry {
94
114
  records = new Map();
95
115
  /** Whether a member may serve: neither it nor its whole account is cooling. */
96
116
  isMemberAvailable(provider, account, model, now = Date.now()) {
97
- return this.isAvailable(accountKey(provider, account), now)
98
- && this.isAvailable(memberKey(provider, account, model), now);
117
+ return (this.isAvailable(accountKey(provider, account), now) &&
118
+ this.isAvailable(memberKey(provider, account, model), now));
99
119
  }
100
120
  /** Whether one registry key is clear right now. */
101
121
  isAvailable(key, now = Date.now()) {
@@ -102,7 +102,8 @@ export class PoolUsageTracker {
102
102
  return { supported: false };
103
103
  const key = `${provider}/${account}`;
104
104
  const entry = this.entries.get(key);
105
- if (entry !== undefined && Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs)) {
105
+ if (entry !== undefined &&
106
+ Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs)) {
106
107
  if (entry.snapshot !== undefined) {
107
108
  if (!force)
108
109
  return entry.snapshot;
@@ -178,29 +179,33 @@ export class PoolUsageTracker {
178
179
  // fetch fails late and would otherwise cache a cooldown over a fresher
179
180
  // success that already landed.
180
181
  const sequence = ++this.nextSequence;
181
- pending = fetcher().then((value) => {
182
+ pending = fetcher()
183
+ .then((value) => {
182
184
  const at = Date.now();
183
185
  const snapshot = { ...value, observedAt: at, stale: false };
184
186
  // An invalidate() during the fetch means this result describes a world
185
187
  // that has moved on; caching it would resurrect the old numbers.
186
- if ((this.generation.get(key) ?? 0) === generation && (this.lastWritten.get(key) ?? 0) < sequence) {
188
+ if ((this.generation.get(key) ?? 0) === generation &&
189
+ (this.lastWritten.get(key) ?? 0) < sequence) {
187
190
  this.lastWritten.set(key, sequence);
188
191
  this.entries.set(key, { snapshot, at });
189
192
  }
190
193
  return snapshot;
191
194
  }, (error) => {
192
- if (!isMissingOrInvalidCredential(error) && (this.generation.get(key) ?? 0) === generation
193
- && (this.lastWritten.get(key) ?? 0) < sequence) {
195
+ if (!isMissingOrInvalidCredential(error) &&
196
+ (this.generation.get(key) ?? 0) === generation &&
197
+ (this.lastWritten.get(key) ?? 0) < sequence) {
194
198
  this.lastWritten.set(key, sequence);
195
199
  this.entries.set(key, {
196
200
  error,
197
201
  at: Date.now(),
198
202
  cooldownMs: cooldownFor(error, this.ttlMs),
199
- ...lastSnapshot === undefined ? {} : { lastSnapshot },
203
+ ...(lastSnapshot === undefined ? {} : { lastSnapshot }),
200
204
  });
201
205
  }
202
206
  throw error;
203
- }).finally(() => {
207
+ })
208
+ .finally(() => {
204
209
  // Only clear our own slot: an invalidate may have replaced it.
205
210
  if (this.inflight.get(key) === pending)
206
211
  this.inflight.delete(key);
@@ -211,7 +216,7 @@ export class PoolUsageTracker {
211
216
  }
212
217
  /** Score one member against a snapshot's windows. */
213
218
  score(member, entry) {
214
- const windows = (entry.snapshot.windows ?? []).filter(window => windowApplies(window, member.model));
219
+ const windows = (entry.snapshot.windows ?? []).filter((window) => windowApplies(window, member.model));
215
220
  let available = true;
216
221
  let urgency = 0;
217
222
  for (const window of windows) {
@@ -235,7 +240,9 @@ function degradedQuota(error) {
235
240
  }
236
241
  /** How long to hold a failure in the negative cache: the endpoint's own `retry-after`, or the default TTL. */
237
242
  function cooldownFor(error, defaultTtlMs) {
238
- return error instanceof OAuthEndpointError && error.retryAfterMs !== undefined ? error.retryAfterMs : defaultTtlMs;
243
+ return error instanceof OAuthEndpointError && error.retryAfterMs !== undefined
244
+ ? error.retryAfterMs
245
+ : defaultTtlMs;
239
246
  }
240
247
  /**
241
248
  * Whether a window constrains this model: unscoped windows always do; a
@@ -105,6 +105,14 @@ export declare class PoolAdapter extends LlmAdapter {
105
105
  private select;
106
106
  /** Pin the serving member to the session (with bounded memory). */
107
107
  private remember;
108
+ /**
109
+ * Build the terminal exhaustion error and log it. Per-member failovers are
110
+ * already warned above; without this line dsh.log shows the failover chain
111
+ * but not the exhaustion that ends it. The message carries only the
112
+ * LlmError's own sanitized text and code — raw provider payloads never
113
+ * reach here (see the note in {@link exhausted}).
114
+ */
115
+ private exhaustedWithWarn;
108
116
  /** Report attempt failures or this pool's active cooldowns, never unrelated health records. */
109
117
  private exhausted;
110
118
  }