dsh-subscriptions 0.2.0 → 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.
- package/README.md +212 -180
- package/lib/auth/claude-code-creds.js +31 -9
- package/lib/auth/device-flow.js +16 -9
- package/lib/auth/oauth-flow.js +13 -10
- package/lib/auth/rpc.js +74 -23
- package/lib/auth/store.js +80 -31
- package/lib/client/CursorCard.d.ts +1 -1
- package/lib/client/CursorCard.js +50 -16
- package/lib/client/ExternalUsageCards.d.ts +1 -1
- package/lib/client/ExternalUsageCards.js +37 -16
- package/lib/client/ImageGallery.d.ts +2 -2
- package/lib/client/ImageGallery.js +92 -33
- package/lib/client/ImageGenerateToolview.js +50 -14
- package/lib/client/OllamaCard.d.ts +1 -1
- package/lib/client/OllamaCard.js +26 -8
- package/lib/client/OllamaSettingsManager.js +113 -30
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +107 -26
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +257 -46
- package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
- package/lib/client/SubscriptionUsageBadge.js +300 -112
- package/lib/client/SubscriptionsSection.js +258 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +62 -24
- package/lib/client/VideoGenerateToolview.js +69 -17
- package/lib/client/account-preferences.d.ts +8 -0
- package/lib/client/account-preferences.js +20 -5
- package/lib/client/host-contract.d.ts +11 -2
- package/lib/client/host-contract.js +20 -3
- package/lib/client/index.d.ts +5 -5
- package/lib/client/index.js +34 -12
- package/lib/client/locales.d.ts +10 -0
- package/lib/client/locales.js +61 -51
- package/lib/client/ollama-draft.js +21 -12
- package/lib/client/reset-action.js +6 -4
- package/lib/client/reset-credits-view.d.ts +1 -1
- package/lib/client/reset-credits-view.js +66 -17
- package/lib/client/subscription-card-styles.js +94 -24
- package/lib/client/usage-color-preferences.js +4 -2
- package/lib/client/usage-pace.js +30 -12
- package/lib/client.js +287 -47
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +118 -64
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +12 -0
- package/lib/provider-settings.js +58 -16
- package/lib/providers/account-preferences.js +51 -22
- package/lib/providers/accounts.js +15 -9
- package/lib/providers/antigravity-oauth-client.js +2 -2
- package/lib/providers/antigravity.js +141 -68
- package/lib/providers/catalog-store.js +120 -41
- package/lib/providers/claude-cli-version.js +3 -3
- package/lib/providers/claude.d.ts +9 -2
- package/lib/providers/claude.js +153 -90
- package/lib/providers/codex-client-version.js +3 -3
- package/lib/providers/codex-search.js +40 -13
- package/lib/providers/codex.js +196 -112
- package/lib/providers/common.js +71 -39
- package/lib/providers/copilot.js +143 -82
- package/lib/providers/cursor-adapter.js +42 -18
- package/lib/providers/cursor-auth.d.ts +1 -1
- package/lib/providers/cursor-auth.js +69 -24
- package/lib/providers/cursor-model-settings.js +14 -5
- package/lib/providers/cursor-usage.js +27 -13
- package/lib/providers/external-usage-controller.js +14 -5
- package/lib/providers/external-usage.js +59 -23
- package/lib/providers/grok.js +152 -89
- package/lib/providers/image-pool.js +29 -11
- package/lib/providers/minimax-usage.js +31 -11
- package/lib/providers/npm-cli-version.js +10 -3
- package/lib/providers/ollama-settings.js +24 -8
- package/lib/providers/ollama-web.js +29 -11
- package/lib/providers/ollama.js +129 -66
- package/lib/providers/pool-family.js +11 -3
- package/lib/providers/pool-health.js +28 -8
- package/lib/providers/pool-usage.js +16 -9
- package/lib/providers/pool.d.ts +8 -0
- package/lib/providers/pool.js +81 -42
- package/lib/providers/rate-limit.js +8 -2
- package/lib/providers/replay.js +40 -17
- package/lib/providers/reset-redemption.js +18 -6
- package/lib/tools/image-generate.js +123 -65
- package/lib/tools/registration.js +5 -2
- package/lib/tools/video-generate.js +51 -31
- package/lib/tools/x-search.js +50 -22
- package/lib/translate/anthropic.js +85 -37
- package/lib/translate/antigravity-schema.js +71 -28
- package/lib/translate/antigravity-thinking.js +22 -9
- package/lib/translate/antigravity.js +141 -50
- package/lib/translate/chat-completions.js +67 -23
- package/lib/translate/resolved.js +55 -21
- package/lib/translate/responses.js +69 -29
- package/lib/translate/sse.js +4 -1
- package/package.json +26 -4
package/lib/providers/ollama.js
CHANGED
|
@@ -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
|
-
{
|
|
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 &&
|
|
97
|
-
|
|
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
|
-
|
|
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 => ({
|
|
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
|
|
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
|
-
: [
|
|
153
|
-
|
|
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 => ({
|
|
168
|
-
|
|
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
|
-
|
|
182
|
-
...apiKey === undefined ? {} : {
|
|
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') &&
|
|
209
|
-
|
|
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
|
|
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
|
|
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)
|
|
253
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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'
|
|
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: {
|
|
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] =
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
? {
|
|
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() {
|
|
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 ??
|
|
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 ??
|
|
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: {
|
|
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
|
|
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
|
-
|
|
523
|
-
|
|
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 = () => {
|
|
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({
|
|
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
|
|
40
|
-
|
|
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([
|
|
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)
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 &&
|
|
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()
|
|
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 &&
|
|
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) &&
|
|
193
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
package/lib/providers/pool.d.ts
CHANGED
|
@@ -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
|
}
|