dsh-subscriptions 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/lib/auth/device-flow.js +6 -3
- package/lib/auth/oauth-flow.js +41 -8
- package/lib/auth/rpc.d.ts +73 -2
- package/lib/auth/rpc.js +53 -3
- package/lib/client/ExternalUsageCards.js +1 -1
- package/lib/client/OllamaCard.d.ts +13 -0
- package/lib/client/OllamaCard.js +76 -0
- package/lib/client/OllamaSettingsManager.d.ts +24 -0
- package/lib/client/OllamaSettingsManager.js +246 -0
- package/lib/client/ProviderModelEditor.js +4 -2
- package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
- package/lib/client/SubscriptionUsageBadge.js +3 -2
- package/lib/client/SubscriptionsSection.d.ts +2 -74
- package/lib/client/SubscriptionsSection.js +4 -67
- package/lib/client/UsageMeter.js +11 -7
- package/lib/client/locales.d.ts +68 -0
- package/lib/client/locales.js +77 -0
- package/lib/client/ollama-draft.d.ts +229 -0
- package/lib/client/ollama-draft.js +246 -0
- package/lib/client/usage-pace.d.ts +7 -0
- package/lib/client/usage-pace.js +15 -0
- package/lib/client.js +1385 -244
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +25 -0
- package/lib/index.js +11060 -9733
- package/lib/model-defaults.d.ts +4 -5
- package/lib/model-defaults.js +18 -14
- package/lib/provider-settings.d.ts +2 -1
- package/lib/provider-settings.js +6 -4
- package/lib/providers/antigravity.d.ts +0 -2
- package/lib/providers/antigravity.js +5 -9
- package/lib/providers/claude.d.ts +14 -1
- package/lib/providers/claude.js +25 -13
- package/lib/providers/codex-search.js +5 -2
- package/lib/providers/codex.js +7 -7
- package/lib/providers/common.d.ts +27 -0
- package/lib/providers/common.js +70 -0
- package/lib/providers/copilot.js +3 -3
- package/lib/providers/cursor-adapter.d.ts +35 -0
- package/lib/providers/cursor-adapter.js +245 -4
- package/lib/providers/cursor-auth.js +11 -4
- package/lib/providers/cursor-usage.js +2 -1
- package/lib/providers/external-usage-controller.d.ts +9 -2
- package/lib/providers/external-usage-controller.js +19 -5
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +93 -8
- package/lib/providers/grok.js +14 -9
- package/lib/providers/minimax-usage.js +2 -1
- package/lib/providers/ollama-settings.d.ts +90 -0
- package/lib/providers/ollama-settings.js +197 -0
- package/lib/providers/ollama-web.d.ts +46 -0
- package/lib/providers/ollama-web.js +213 -0
- package/lib/providers/ollama.d.ts +107 -0
- package/lib/providers/ollama.js +545 -0
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +0 -5
- package/lib/tools/video-generate.js +25 -14
- package/lib/tools/x-search.js +2 -2
- package/lib/translate/resolved.d.ts +5 -2
- package/lib/translate/resolved.js +5 -2
- package/package.json +1 -1
package/lib/model-defaults.d.ts
CHANGED
|
@@ -6,12 +6,11 @@ export type ModelDefaults = Readonly<Partial<Record<ProviderId, ModelDefaultMap>
|
|
|
6
6
|
/** Absolute path of the defaults file. */
|
|
7
7
|
export declare function modelDefaultsFilePath(): string;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Ready the defaults store.
|
|
10
|
+
* index.ts calls it at apply time so every later synchronous read sees the
|
|
11
|
+
* persisted state; store tests also call it after resetting the singleton.
|
|
11
12
|
*/
|
|
12
|
-
export declare function
|
|
13
|
-
/** A detached snapshot for the RPC surface (render + diffing). */
|
|
14
|
-
export declare function modelDefaultsSnapshot(): ModelDefaults;
|
|
13
|
+
export declare function loadModelDefaults(): Promise<void>;
|
|
15
14
|
/**
|
|
16
15
|
* Set or clear one model's configured default effort, then persist. The
|
|
17
16
|
* memory snapshot updates only after the atomic write succeeds, so a failed
|
package/lib/model-defaults.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* chip the model picker shows when the discovered catalog advertises no
|
|
9
9
|
* default at all.
|
|
10
10
|
*
|
|
11
|
-
* Writes are single-process and atomic
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Writes are single-process and atomic. The account manager submits effort
|
|
12
|
+
* edits sequentially and disables its editor while saving, but concurrent RPC
|
|
13
|
+
* callers can still overlap. The write chain below serialises them, so no
|
|
14
|
+
* update is lost to a read-modify-write race. Every read comes from the
|
|
15
15
|
* in-memory snapshot, so the on-disk file only needs to survive a restart: a
|
|
16
16
|
* malformed file reads as empty and is rewritten on the next save, never
|
|
17
17
|
* taking the plugin down with it.
|
|
@@ -29,21 +29,21 @@ const EMPTY = Object.freeze({});
|
|
|
29
29
|
let current = EMPTY;
|
|
30
30
|
/** One lazy load of the on-disk file (read once per process). */
|
|
31
31
|
let ready;
|
|
32
|
-
/** Last load failure
|
|
32
|
+
/** Last load failure or skipped-entry warning, inspected only by tests. */
|
|
33
33
|
let loadError;
|
|
34
34
|
/**
|
|
35
35
|
* Serialises every write: the read-modify-write sequence must not interleave,
|
|
36
36
|
* or a fast second save would compute its snapshot from the stale `current`
|
|
37
|
-
* and silently drop the first update
|
|
38
|
-
*
|
|
37
|
+
* and silently drop the first update. Concurrent RPC callers can overlap
|
|
38
|
+
* even though one account-manager dialog submits its edits sequentially.
|
|
39
39
|
*/
|
|
40
40
|
let writeChain = Promise.resolve();
|
|
41
41
|
/**
|
|
42
42
|
* Validate one persisted provider section: a string→string map, or undefined.
|
|
43
43
|
* Malformed *entries* are skipped, not the whole section: one bad value (a
|
|
44
44
|
* hand edit losing its quotes) must not silently un-configure every model in
|
|
45
|
-
* that provider.
|
|
46
|
-
*
|
|
45
|
+
* that provider. Dropped model ids are recorded in a test-only load warning;
|
|
46
|
+
* production callers do not surface this warning.
|
|
47
47
|
*/
|
|
48
48
|
function sanitizeProvider(value, dropped) {
|
|
49
49
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
@@ -136,15 +136,16 @@ function sectionOf(defaults, provider) {
|
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
138
138
|
* Ready the defaults store.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
139
|
+
* index.ts calls it at apply time so every later synchronous read sees the
|
|
140
|
+
* persisted state; store tests also call it after resetting the singleton.
|
|
141
141
|
*/
|
|
142
142
|
export async function loadModelDefaults() {
|
|
143
143
|
await ensureReady();
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
146
|
+
* Test-only observation of the last load failure or skipped-entry warning.
|
|
147
|
+
* No production diagnostic consumer currently reads it.
|
|
148
|
+
* @internal Exported for tests only.
|
|
148
149
|
*/
|
|
149
150
|
export function modelDefaultsLoadError() {
|
|
150
151
|
return loadError;
|
|
@@ -164,7 +165,10 @@ export function defaultEffortOf(provider, model) {
|
|
|
164
165
|
// breaks that model's resolution).
|
|
165
166
|
return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : undefined;
|
|
166
167
|
}
|
|
167
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Detached snapshot for store assertions in tests; no production RPC uses it.
|
|
170
|
+
* @internal Exported for tests only.
|
|
171
|
+
*/
|
|
168
172
|
export function modelDefaultsSnapshot() {
|
|
169
173
|
const result = {};
|
|
170
174
|
for (const provider of PROVIDER_IDS) {
|
|
@@ -32,7 +32,8 @@ export declare class ProviderSettingsStore {
|
|
|
32
32
|
constructor(path?: string);
|
|
33
33
|
get(provider: ProviderId): ProviderPreferences;
|
|
34
34
|
visible(provider: ProviderId, model: string): boolean;
|
|
35
|
-
|
|
35
|
+
/** The user's local context-window override for one model; absent follows the provider. */
|
|
36
|
+
contextWindow(model: string, provider?: ProviderId): number | undefined;
|
|
36
37
|
/** Creation-time policy survives restarts and never changes an existing session. */
|
|
37
38
|
toolEnabled(provider: ProviderId, tool: SubscriptionTool, createdAt?: number): boolean;
|
|
38
39
|
set(provider: ProviderId, input: unknown): Promise<void>;
|
package/lib/provider-settings.js
CHANGED
|
@@ -51,8 +51,9 @@ export function validatePreferences(provider, input) {
|
|
|
51
51
|
result.visibleModels = [...new Set(raw.visibleModels)];
|
|
52
52
|
}
|
|
53
53
|
if (raw.contextWindows !== undefined) {
|
|
54
|
-
if (provider !== 'codex')
|
|
55
|
-
throw new Error('context window overrides are
|
|
54
|
+
if (provider !== 'codex' && provider !== 'claude') {
|
|
55
|
+
throw new Error('context window overrides are supported only for Codex and Claude');
|
|
56
|
+
}
|
|
56
57
|
if (!raw.contextWindows || typeof raw.contextWindows !== 'object' || Array.isArray(raw.contextWindows)) {
|
|
57
58
|
throw new Error('contextWindows must be a model-to-token map');
|
|
58
59
|
}
|
|
@@ -110,8 +111,9 @@ export class ProviderSettingsStore {
|
|
|
110
111
|
visible(provider, model) {
|
|
111
112
|
return this.current.providers[provider]?.visibleModels?.includes(model) ?? true;
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
/** The user's local context-window override for one model; absent follows the provider. */
|
|
115
|
+
contextWindow(model, provider = 'codex') {
|
|
116
|
+
const windows = this.current.providers[provider]?.contextWindows;
|
|
115
117
|
return windows && Object.hasOwn(windows, model) ? windows[model] : undefined;
|
|
116
118
|
}
|
|
117
119
|
/** Creation-time policy survives restarts and never changes an existing session. */
|
|
@@ -60,8 +60,6 @@ export declare function isAntigravityPermanentRefreshError(error: unknown): bool
|
|
|
60
60
|
export declare function fetchAntigravityModels(session: AntigravitySession, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
61
61
|
/** Fetch plan and per-model quota windows when the upstream exposes them. */
|
|
62
62
|
export declare function fetchAntigravityUsage(session: AntigravitySession, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
63
|
-
/** URL for either v1internal generation transport. */
|
|
64
|
-
export declare function antigravityGenerateURL(baseURL: string | undefined, stream: boolean): string;
|
|
65
63
|
/** Forward one already-built payload to generateContent or streamGenerateContent. */
|
|
66
64
|
export declare function requestAntigravityContent(session: AntigravitySession, payload: AntigravityRequest, stream: boolean, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<Response>;
|
|
67
65
|
export interface AntigravityAdapterOptions {
|
|
@@ -7,7 +7,7 @@ import { errorChain, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError } from '@deepseek
|
|
|
7
7
|
import { resolveImages } from '../translate/resolved.js';
|
|
8
8
|
import { antigravityReasoning } from '../translate/antigravity-thinking.js';
|
|
9
9
|
import { parseAntigravityResponse, streamAntigravity, toAntigravityRequest, } from '../translate/antigravity.js';
|
|
10
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, discoverAcrossAccounts, isDiscoveryAborted, } from './common.js';
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, discoverAcrossAccounts, isDiscoveryAborted, } from './common.js';
|
|
11
11
|
import { hostFetch } from '../http.js';
|
|
12
12
|
import { ANTIGRAVITY_DEFAULT_CLIENT_ID, ANTIGRAVITY_DEFAULT_CLIENT_SECRET } from './antigravity-oauth-client.js';
|
|
13
13
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
@@ -126,7 +126,7 @@ async function callInternal(method, body, accessToken, runtime, fetchFn, signal)
|
|
|
126
126
|
}, runtime, fetchFn);
|
|
127
127
|
if (!response.ok)
|
|
128
128
|
throw await httpLlmError(response, `Antigravity ${method}`);
|
|
129
|
-
return response
|
|
129
|
+
return parseProviderJson(response, `Antigravity ${method}`);
|
|
130
130
|
}
|
|
131
131
|
function projectIdOf(value) {
|
|
132
132
|
if (typeof value === 'string' && value.length > 0)
|
|
@@ -217,7 +217,7 @@ export async function exchangeAntigravityCode(code, verifier, redirectUri, oauth
|
|
|
217
217
|
});
|
|
218
218
|
if (!response.ok)
|
|
219
219
|
throw await oauthEndpointError(response, 'Antigravity');
|
|
220
|
-
const tokens = await response
|
|
220
|
+
const tokens = await parseProviderJson(response, 'Antigravity token exchange');
|
|
221
221
|
if (typeof tokens.access_token !== 'string')
|
|
222
222
|
throw new Error('Antigravity token endpoint returned no access token');
|
|
223
223
|
const account = await discoverAntigravityAccount(tokens.access_token, runtime, fetchFn);
|
|
@@ -238,7 +238,7 @@ export async function refreshAntigravity(session, oauth, fetchFn = hostFetch) {
|
|
|
238
238
|
});
|
|
239
239
|
if (!response.ok)
|
|
240
240
|
throw await oauthEndpointError(response, 'Antigravity');
|
|
241
|
-
return sessionFromTokens(await response
|
|
241
|
+
return sessionFromTokens(await parseProviderJson(response, 'Antigravity token refresh'), {
|
|
242
242
|
projectId: session.projectId,
|
|
243
243
|
...session.account === undefined ? {} : { account: session.account },
|
|
244
244
|
...session.plan === undefined ? {} : { plan: session.plan },
|
|
@@ -313,10 +313,6 @@ export async function fetchAntigravityUsage(session, runtime = {}, fetchFn = hos
|
|
|
313
313
|
...displayPlan === undefined ? {} : { plan: displayPlan },
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
|
-
/** URL for either v1internal generation transport. */
|
|
317
|
-
export function antigravityGenerateURL(baseURL, stream) {
|
|
318
|
-
return `${antigravityBaseURL(baseURL)}/v1internal:${stream ? 'streamGenerateContent?alt=sse' : 'generateContent'}`;
|
|
319
|
-
}
|
|
320
316
|
/** Forward one already-built payload to generateContent or streamGenerateContent. */
|
|
321
317
|
export async function requestAntigravityContent(session, payload, stream, runtime = {}, fetchFn = hostFetch, signal) {
|
|
322
318
|
return fetchAntigravity(stream ? 'streamGenerateContent?alt=sse' : 'generateContent', {
|
|
@@ -511,6 +507,6 @@ export class AntigravityAdapter extends LlmAdapter {
|
|
|
511
507
|
const response = await requestAntigravityContent(session, toAntigravityRequest(options, messages, session.projectId), false, this.options.runtime, this.options.fetchFn, options.signal);
|
|
512
508
|
if (!response.ok)
|
|
513
509
|
throw await httpLlmError(response, 'Antigravity API');
|
|
514
|
-
return parseAntigravityResponse(await response
|
|
510
|
+
return parseAntigravityResponse(await parseProviderJson(response, 'Antigravity generateContent'));
|
|
515
511
|
}
|
|
516
512
|
}
|
|
@@ -127,6 +127,12 @@ export interface ClaudeAdapterOptions {
|
|
|
127
127
|
* the provider's own default.
|
|
128
128
|
*/
|
|
129
129
|
defaultEffortOf?: (model: string) => string | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* The user's local context-window override for one model (the Settings
|
|
132
|
+
* editor). Returns undefined to follow the advertised window. The result
|
|
133
|
+
* is capped at that window: Anthropic advertises no separate maximum.
|
|
134
|
+
*/
|
|
135
|
+
contextWindowOf?: (model: string) => number | undefined;
|
|
130
136
|
/** Resolves the Claude Code version to present (npm-backed); absent means the local floor. */
|
|
131
137
|
resolveCliVersion?: () => Promise<string>;
|
|
132
138
|
/** Cache breakpoint lifetime; absent means the API's five-minute default. */
|
|
@@ -183,7 +189,14 @@ export declare class ClaudeAdapter extends LlmAdapter {
|
|
|
183
189
|
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
184
190
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
185
191
|
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
186
|
-
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
192
|
+
resolveOwnModel(provider: string, model: string, account?: string): Promise<LlmResolvedModelInfo>;
|
|
193
|
+
/** The advertised window is both the default and the ceiling for a local override. */
|
|
194
|
+
private contextLimitsFor;
|
|
195
|
+
/** Account-specific bounds the Settings editor shows next to a model's override input. */
|
|
196
|
+
contextLimits(model: string, account?: string): Promise<{
|
|
197
|
+
default: number;
|
|
198
|
+
max: number;
|
|
199
|
+
}>;
|
|
187
200
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
188
201
|
/** Pool seam: stream through one specific account instead of the default. */
|
|
189
202
|
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
package/lib/providers/claude.js
CHANGED
|
@@ -7,7 +7,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
7
7
|
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
import { hostSupportsImageOffload, IMAGE_OFFLOAD_REQUIRED, requiredImageOffloadCount, resolveImages } from '../translate/resolved.js';
|
|
9
9
|
import { markMessageCache, streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
|
|
10
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
11
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
12
12
|
import { hostFetch } from '../http.js';
|
|
13
13
|
import { compareVersions } from './npm-cli-version.js';
|
|
@@ -212,7 +212,7 @@ export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
|
|
|
212
212
|
});
|
|
213
213
|
if (!response.ok)
|
|
214
214
|
throw await oauthEndpointError(response, 'claude');
|
|
215
|
-
return claudeSession(await response
|
|
215
|
+
return claudeSession(await parseProviderJson(response, 'claude token exchange'), undefined, true);
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
218
|
* Refresh a claude session (JSON grant echoing the issued scope).
|
|
@@ -232,7 +232,7 @@ export async function refreshClaude(session) {
|
|
|
232
232
|
});
|
|
233
233
|
if (!response.ok)
|
|
234
234
|
throw await oauthEndpointError(response, 'claude');
|
|
235
|
-
const next = await claudeSession(await response
|
|
235
|
+
const next = await claudeSession(await parseProviderJson(response, 'claude token refresh'), session.refreshToken, false);
|
|
236
236
|
return {
|
|
237
237
|
...next,
|
|
238
238
|
...session.emailAddress === undefined ? {} : { emailAddress: session.emailAddress },
|
|
@@ -398,7 +398,7 @@ export async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cli
|
|
|
398
398
|
}
|
|
399
399
|
if (!response.ok)
|
|
400
400
|
throw await oauthEndpointError(response, 'claude usage');
|
|
401
|
-
const payload = await response
|
|
401
|
+
const payload = await parseProviderJson(response, 'claude usage');
|
|
402
402
|
const resetCredits = resetCreditsError === undefined ? claudeResetCredits(payload.cedar_ember, Date.now()) : undefined;
|
|
403
403
|
const resets = {
|
|
404
404
|
...resetCredits === undefined ? {} : { resetCredits },
|
|
@@ -462,7 +462,7 @@ export async function fetchClaudeModels(session, fetchFn = hostFetch, signal, cl
|
|
|
462
462
|
});
|
|
463
463
|
if (!response.ok)
|
|
464
464
|
throw await httpLlmError(response, 'claude models API');
|
|
465
|
-
const payload = await response
|
|
465
|
+
const payload = await parseProviderJson(response, 'claude models API');
|
|
466
466
|
if (!Array.isArray(payload.data)) {
|
|
467
467
|
throw new Error('claude models API returned an invalid catalog');
|
|
468
468
|
}
|
|
@@ -600,10 +600,14 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
600
600
|
}
|
|
601
601
|
return cache;
|
|
602
602
|
}
|
|
603
|
-
async discovered(model) {
|
|
603
|
+
async discovered(model, account) {
|
|
604
604
|
if (!this.options.discovery)
|
|
605
605
|
return undefined;
|
|
606
|
-
|
|
606
|
+
// A named account is looked up alone: its window must not be borrowed
|
|
607
|
+
// from another account's catalog.
|
|
608
|
+
const accounts = account === undefined
|
|
609
|
+
? (await this.options.tokens.list()).map(entry => entry.key)
|
|
610
|
+
: [account];
|
|
607
611
|
return discoverAcrossAccounts(accounts, async (account) => {
|
|
608
612
|
const catalog = await this.catalogFor(account);
|
|
609
613
|
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
@@ -674,22 +678,30 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
674
678
|
return this.resolveOwnModel(provider, model);
|
|
675
679
|
}
|
|
676
680
|
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
677
|
-
async resolveOwnModel(provider, model) {
|
|
678
|
-
const disc = await this.discovered(model);
|
|
681
|
+
async resolveOwnModel(provider, model, account) {
|
|
682
|
+
const disc = await this.discovered(model, account);
|
|
679
683
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
684
|
+
const limits = this.contextLimitsFor(disc, configured);
|
|
680
685
|
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), disc?.reasoning);
|
|
681
686
|
return {
|
|
682
687
|
provider,
|
|
683
688
|
id: model,
|
|
684
689
|
name: disc?.name ?? configured?.name ?? model,
|
|
685
690
|
inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
|
|
686
|
-
context: {
|
|
687
|
-
contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW,
|
|
688
|
-
},
|
|
691
|
+
context: { contextWindow: Math.min(this.options.contextWindowOf?.(model) ?? limits.default, limits.max) },
|
|
689
692
|
defaultMaxTokens: claudeMaxTokens(configured, disc),
|
|
690
693
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
691
694
|
};
|
|
692
695
|
}
|
|
696
|
+
/** The advertised window is both the default and the ceiling for a local override. */
|
|
697
|
+
contextLimitsFor(disc, configured) {
|
|
698
|
+
const advertised = disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW;
|
|
699
|
+
return { default: advertised, max: advertised };
|
|
700
|
+
}
|
|
701
|
+
/** Account-specific bounds the Settings editor shows next to a model's override input. */
|
|
702
|
+
async contextLimits(model, account) {
|
|
703
|
+
return this.contextLimitsFor(await this.discovered(model, account), this.options.models.find(entry => entry.id === model));
|
|
704
|
+
}
|
|
693
705
|
async *stream(options) {
|
|
694
706
|
const pool = this.options.pool?.();
|
|
695
707
|
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
@@ -752,7 +764,7 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
752
764
|
if (hostSupportsImageOffload()) {
|
|
753
765
|
const offloadImages = requiredImageOffloadCount(messages, CLAUDE_REQUEST_IMAGE_BUDGET);
|
|
754
766
|
if (offloadImages > 0) {
|
|
755
|
-
//
|
|
767
|
+
// The supported 0.1.7-rc.2 floor reads `offloadImages`; older option types lack it.
|
|
756
768
|
throw new LlmError(`claude request images exceed the ${CLAUDE_REQUEST_IMAGE_BUDGET}-byte base64 budget; `
|
|
757
769
|
+ `${offloadImages} more oldest image(s) must be offloaded`, IMAGE_OFFLOAD_REQUIRED, { offloadImages });
|
|
758
770
|
}
|
|
@@ -86,8 +86,11 @@ export class CodexWebSearchProvider {
|
|
|
86
86
|
try {
|
|
87
87
|
return JSON.parse(text);
|
|
88
88
|
}
|
|
89
|
-
catch
|
|
90
|
-
|
|
89
|
+
catch {
|
|
90
|
+
// No cause: the host expands causes recursively, and Node's parse error
|
|
91
|
+
// quotes a bounded excerpt of the body, so attaching it would relocate
|
|
92
|
+
// the leak rather than remove it.
|
|
93
|
+
throw new WebError('Codex Web Search returned invalid JSON', 'CODEX_SEARCH_RESPONSE');
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
throw new WebError('Codex Web Search exhausted its retry policy', 'CODEX_SEARCH_UPSTREAM');
|
package/lib/providers/codex.js
CHANGED
|
@@ -8,7 +8,7 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
|
|
|
8
8
|
import { decodeJwtPayload } from '../auth/jwt.js';
|
|
9
9
|
import { resolveImages } from '../translate/resolved.js';
|
|
10
10
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
11
|
-
import { deterministicSessionId, effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isDiscoveryAuthFailure, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
|
+
import { deterministicSessionId, effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isDiscoveryAuthFailure, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
12
12
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
13
13
|
import { hostFetch } from '../http.js';
|
|
14
14
|
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
@@ -185,7 +185,7 @@ export async function exchangeCodexCode(code, verifier, redirectUri) {
|
|
|
185
185
|
});
|
|
186
186
|
if (!response.ok)
|
|
187
187
|
throw await oauthEndpointError(response, 'codex');
|
|
188
|
-
return codexSession(await response
|
|
188
|
+
return codexSession(await parseProviderJson(response, 'codex token exchange'));
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
191
|
* Refresh a codex session (JSON grant — unlike the code exchange).
|
|
@@ -204,7 +204,7 @@ export async function refreshCodex(session) {
|
|
|
204
204
|
});
|
|
205
205
|
if (!response.ok)
|
|
206
206
|
throw await oauthEndpointError(response, 'codex');
|
|
207
|
-
return codexSession(await response
|
|
207
|
+
return codexSession(await parseProviderJson(response, 'codex token refresh'), session);
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
210
|
* Whether a codex refresh failure means the login is permanently gone.
|
|
@@ -239,7 +239,7 @@ export async function fetchCodexResetCredits(session, fetchFn = hostFetch, signa
|
|
|
239
239
|
});
|
|
240
240
|
if (!response.ok)
|
|
241
241
|
throw await oauthEndpointError(response, 'codex reset credits');
|
|
242
|
-
const payload = await response
|
|
242
|
+
const payload = await parseProviderJson(response, 'codex reset credits');
|
|
243
243
|
if (!Array.isArray(payload?.credits))
|
|
244
244
|
throw new Error('codex reset credits: unexpected response (missing credits array)');
|
|
245
245
|
return payload.credits.flatMap((value) => {
|
|
@@ -269,7 +269,7 @@ export async function consumeCodexResetCredit(session, creditId, requestId, fetc
|
|
|
269
269
|
});
|
|
270
270
|
if (!response.ok)
|
|
271
271
|
throw await oauthEndpointError(response, 'codex consume reset');
|
|
272
|
-
const payload = await response
|
|
272
|
+
const payload = await parseProviderJson(response, 'codex consume reset');
|
|
273
273
|
if (payload?.code !== 'reset')
|
|
274
274
|
throw new Error('Reset was not confirmed. Verify the account in Codex before retrying.');
|
|
275
275
|
}
|
|
@@ -347,7 +347,7 @@ export async function fetchCodexUsage(session, fetchFn = hostFetch, signal) {
|
|
|
347
347
|
});
|
|
348
348
|
if (!response.ok)
|
|
349
349
|
throw await oauthEndpointError(response, 'codex usage');
|
|
350
|
-
const payload = await response
|
|
350
|
+
const payload = await parseProviderJson(response, 'codex usage');
|
|
351
351
|
const windows = [];
|
|
352
352
|
const primary = codexUsageWindow(payload.rate_limit?.primary_window, 'session');
|
|
353
353
|
const secondary = codexUsageWindow(payload.rate_limit?.secondary_window, 'weekly');
|
|
@@ -410,7 +410,7 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
|
|
|
410
410
|
});
|
|
411
411
|
if (!response.ok)
|
|
412
412
|
throw await oauthEndpointError(response, 'codex models');
|
|
413
|
-
const payload = await response
|
|
413
|
+
const payload = await parseProviderJson(response, 'codex models');
|
|
414
414
|
if (!Array.isArray(payload.models))
|
|
415
415
|
throw new Error('codex models endpoint returned no models array');
|
|
416
416
|
const discovered = [];
|
|
@@ -33,6 +33,26 @@ export interface ModelEntry {
|
|
|
33
33
|
* @returns the validated entries.
|
|
34
34
|
*/
|
|
35
35
|
export declare function validateModels(models: readonly ModelEntry[], label: string): ModelEntry[];
|
|
36
|
+
/**
|
|
37
|
+
* Parse a provider response or stored JSON string without exposing input excerpts.
|
|
38
|
+
* Success returns the same JSON value; only SyntaxError is replaced, preserving
|
|
39
|
+
* parse-failure type checks and leaving body-read/abort failures unchanged.
|
|
40
|
+
* Node's Unexpected token errors include a bounded input excerpt. Do not retain
|
|
41
|
+
* the original error as cause: the host displays causes recursively.
|
|
42
|
+
* @param input - response to consume once, or JSON text to parse.
|
|
43
|
+
* @param origin - local endpoint/operation label; never response.url, a discovered
|
|
44
|
+
* URL, a credential, or any other provider-controlled text.
|
|
45
|
+
* @returns the parsed value, without validation or credential filtering.
|
|
46
|
+
* @throws SyntaxError with the local origin and a fixed omission marker.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseProviderJson<T = unknown>(input: Response | string, origin: string): Promise<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Read a bounded JSON body; oversized or malformed payloads fail loudly.
|
|
51
|
+
* Bytes accumulate per chunk and the stream is cancelled past the cap, so
|
|
52
|
+
* the bound limits memory even when the body has no end; a declared
|
|
53
|
+
* over-cap `content-length` fails before the first read.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readBoundedJson(response: Response, label: string, maxBytes: number): Promise<unknown>;
|
|
36
56
|
/** Optional per-call hooks {@link httpLlmError} uses to read a rate-limit window. */
|
|
37
57
|
export interface HttpLlmErrorOptions {
|
|
38
58
|
/**
|
|
@@ -199,6 +219,13 @@ export interface UsageWindow {
|
|
|
199
219
|
windowDurationMs?: number;
|
|
200
220
|
/** Provider-verified fixed interval; permits deriving start from end and duration. */
|
|
201
221
|
fixedWindow?: boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Display-only start the reader assumed (kind or label based, clamped
|
|
224
|
+
* to now). The time marker may ride it; pace and color must never —
|
|
225
|
+
* it is not evidence of when the window opened. Mutually exclusive
|
|
226
|
+
* with `startsAt`.
|
|
227
|
+
*/
|
|
228
|
+
displayStartsAt?: number;
|
|
202
229
|
}
|
|
203
230
|
/** One banked reset credit (Codex), or one grant containing resets (Claude). */
|
|
204
231
|
export interface ResetCredit {
|
package/lib/providers/common.js
CHANGED
|
@@ -61,6 +61,76 @@ export function validateModels(models, label) {
|
|
|
61
61
|
function providerDiagnostic(summary, hasBody) {
|
|
62
62
|
return hasBody ? `${summary}: [provider response body omitted]` : summary;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse a provider response or stored JSON string without exposing input excerpts.
|
|
66
|
+
* Success returns the same JSON value; only SyntaxError is replaced, preserving
|
|
67
|
+
* parse-failure type checks and leaving body-read/abort failures unchanged.
|
|
68
|
+
* Node's Unexpected token errors include a bounded input excerpt. Do not retain
|
|
69
|
+
* the original error as cause: the host displays causes recursively.
|
|
70
|
+
* @param input - response to consume once, or JSON text to parse.
|
|
71
|
+
* @param origin - local endpoint/operation label; never response.url, a discovered
|
|
72
|
+
* URL, a credential, or any other provider-controlled text.
|
|
73
|
+
* @returns the parsed value, without validation or credential filtering.
|
|
74
|
+
* @throws SyntaxError with the local origin and a fixed omission marker.
|
|
75
|
+
*/
|
|
76
|
+
export async function parseProviderJson(input, origin) {
|
|
77
|
+
try {
|
|
78
|
+
return (typeof input === 'string' ? JSON.parse(input) : await input.json());
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof SyntaxError))
|
|
82
|
+
throw error;
|
|
83
|
+
throw new SyntaxError(providerDiagnostic(`${origin}: invalid JSON`, true));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read a bounded JSON body; oversized or malformed payloads fail loudly.
|
|
88
|
+
* Bytes accumulate per chunk and the stream is cancelled past the cap, so
|
|
89
|
+
* the bound limits memory even when the body has no end; a declared
|
|
90
|
+
* over-cap `content-length` fails before the first read.
|
|
91
|
+
*/
|
|
92
|
+
export async function readBoundedJson(response, label, maxBytes) {
|
|
93
|
+
const declared = Number(response.headers.get('content-length'));
|
|
94
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
95
|
+
// Stop the socket too: the headers arrived, but the body must never flow.
|
|
96
|
+
await response.body?.cancel().catch(() => undefined);
|
|
97
|
+
throw new Error(`${label} response exceeds ${String(maxBytes)} bytes`);
|
|
98
|
+
}
|
|
99
|
+
if (response.body === null)
|
|
100
|
+
throw new Error(`${label} response has no body`);
|
|
101
|
+
const reader = response.body.getReader();
|
|
102
|
+
const decoder = new TextDecoder();
|
|
103
|
+
let bytes = 0;
|
|
104
|
+
let text = '';
|
|
105
|
+
try {
|
|
106
|
+
for (;;) {
|
|
107
|
+
const chunk = await reader.read();
|
|
108
|
+
if (chunk.done)
|
|
109
|
+
break;
|
|
110
|
+
bytes += chunk.value.byteLength;
|
|
111
|
+
if (bytes > maxBytes) {
|
|
112
|
+
// Cancel before throwing so the live stream does not linger behind
|
|
113
|
+
// a rejection the caller already stopped listening to.
|
|
114
|
+
await reader.cancel().catch(() => undefined);
|
|
115
|
+
throw new Error(`${label} response exceeds ${String(maxBytes)} bytes`);
|
|
116
|
+
}
|
|
117
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
118
|
+
}
|
|
119
|
+
text += decoder.decode();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
try {
|
|
123
|
+
reader.releaseLock();
|
|
124
|
+
}
|
|
125
|
+
catch { /* best effort */ }
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(text);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new Error(`${label} response is not JSON`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
64
134
|
/**
|
|
65
135
|
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
66
136
|
* stable code and, for a rate-limited request, the disclosed reset instant to
|
package/lib/providers/copilot.js
CHANGED
|
@@ -17,7 +17,7 @@ import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortI
|
|
|
17
17
|
import { resolveImages } from '../translate/resolved.js';
|
|
18
18
|
import { streamChatCompletions, toChatMessages, toChatTools, } from '../translate/chat-completions.js';
|
|
19
19
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
20
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
20
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
21
21
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
22
22
|
import { hostFetch } from '../http.js';
|
|
23
23
|
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
@@ -144,7 +144,7 @@ export async function exchangeCopilotToken(githubToken, fetchFn = hostFetch) {
|
|
|
144
144
|
});
|
|
145
145
|
if (!response.ok)
|
|
146
146
|
throw await oauthEndpointError(response, 'copilot');
|
|
147
|
-
const wire = await response
|
|
147
|
+
const wire = await parseProviderJson(response, 'copilot token exchange');
|
|
148
148
|
if (typeof wire.token !== 'string' || wire.token.length === 0) {
|
|
149
149
|
throw new Error('copilot token endpoint returned no token');
|
|
150
150
|
}
|
|
@@ -268,7 +268,7 @@ export async function fetchCopilotModels(session, fetchFn = hostFetch, signal) {
|
|
|
268
268
|
});
|
|
269
269
|
if (!response.ok)
|
|
270
270
|
throw await oauthEndpointError(response, 'copilot models');
|
|
271
|
-
const payload = await response
|
|
271
|
+
const payload = await parseProviderJson(response, 'copilot models');
|
|
272
272
|
if (!Array.isArray(payload.data))
|
|
273
273
|
throw new Error('copilot models endpoint returned no data array');
|
|
274
274
|
const seen = new Set();
|
|
@@ -5,8 +5,43 @@ import { CursorAdapter } from 'dsh-subscriptions/cursor-transport';
|
|
|
5
5
|
* blocks. DSH 0.1.7 carries them as first-class role=tool messages instead.
|
|
6
6
|
*/
|
|
7
7
|
export declare function projectCursorMessages(messages: GenerateOptions['messages']): unknown[];
|
|
8
|
+
/**
|
|
9
|
+
* Cursor adapter for the DSH host.
|
|
10
|
+
*
|
|
11
|
+
* Failure text contract: `finish/error` messages that originate from the
|
|
12
|
+
* vendor stream and can quote provider-controlled text (the response content
|
|
13
|
+
* type, trailers, end-stream errors, a frame-reader or other error whose message
|
|
14
|
+
* is not one of the literals below, and a transport-coded failure that is not a
|
|
15
|
+
* socket-layer error) are replaced by `<code> [provider response text omitted]`. Failures that are
|
|
16
|
+
* wholly local keep their own message and code:
|
|
17
|
+
*
|
|
18
|
+
* - anything the auth service threw (not signed in, sign-in expired, token
|
|
19
|
+
* refresh status), and a host attachment read failure;
|
|
20
|
+
* - the vendor's own request validation, by code (`UNSUPPORTED_OPTION`,
|
|
21
|
+
* `UNSUPPORTED_CONTENT`, `TOOL_LIMIT`);
|
|
22
|
+
* - the vendor's fixed-literal plain errors, by exact message: the HTTP status
|
|
23
|
+
* (`Cursor agent returned HTTP 429`), the response, idle and progress
|
|
24
|
+
* timeouts, a closed bridge or stream, an unreadable compressed frame, and
|
|
25
|
+
* an invalid Cursor setting;
|
|
26
|
+
* - a network error raised by the operating system (`ECONNREFUSED`,
|
|
27
|
+
* `ENOTFOUND`, ...), recognised from the thrown error's own code, not from
|
|
28
|
+
* the vendor's classification.
|
|
29
|
+
*
|
|
30
|
+
* Not a provider-origin class: a network error such as `ECONNREFUSED` is a
|
|
31
|
+
* local failure (no network, daemon down, DNS), and is kept. What is still
|
|
32
|
+
* replaced although it is probably local is a transport failure with no
|
|
33
|
+
* socket-layer errno: a TLS or certificate failure and an HTTP/2 protocol
|
|
34
|
+
* error. The vendor's `TRANSPORT` code cannot decide those, because
|
|
35
|
+
* `classifyCursorError` derives it from message text (`network`, `connection`,
|
|
36
|
+
* `socket`, `fetch`, `ECONN`, `http2`) and a provider's end-stream message or
|
|
37
|
+
* `grpc-message` can contain any of them; so the code is never trusted alone.
|
|
38
|
+
* Telling these apart would need a check on the thrown error object, as the
|
|
39
|
+
* errno check above does, extended to the TLS and HTTP/2 error codes. That is
|
|
40
|
+
* not done here and is not verified.
|
|
41
|
+
*/
|
|
8
42
|
export declare class CursorCompatAdapter extends CursorAdapter {
|
|
9
43
|
private readonly visibleModels;
|
|
44
|
+
private readonly localFailures;
|
|
10
45
|
constructor(options: ConstructorParameters<typeof CursorAdapter>[0] & {
|
|
11
46
|
visibleModels?: () => readonly string[] | undefined;
|
|
12
47
|
});
|