dsh-subscriptions 0.1.3 → 0.1.5
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 +64 -0
- package/lib/auth/device-flow.js +6 -3
- package/lib/auth/oauth-flow.js +41 -8
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- package/lib/client/ProviderAccountManager.js +34 -41
- package/lib/client/ProviderModelEditor.js +5 -9
- package/lib/client/SpeedSelect.js +8 -7
- package/lib/client/SubscriptionUsageBadge.js +6 -5
- package/lib/client/SubscriptionsSection.d.ts +21 -73
- package/lib/client/SubscriptionsSection.js +130 -104
- package/lib/client/UsageBadgeDisplaySetting.js +4 -8
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +58 -0
- package/lib/client/locales.js +58 -0
- package/lib/client/provider-settings-styles.d.ts +3 -0
- package/lib/client/provider-settings-styles.js +206 -0
- package/lib/client/reset-action.d.ts +14 -0
- package/lib/client/reset-action.js +15 -0
- package/lib/client/reset-credits-view.d.ts +53 -0
- package/lib/client/reset-credits-view.js +103 -0
- package/lib/client.js +937 -310
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +6058 -5298
- package/lib/model-defaults.d.ts +4 -5
- package/lib/model-defaults.js +18 -14
- package/lib/providers/account-preferences.js +3 -2
- package/lib/providers/antigravity.d.ts +0 -2
- package/lib/providers/antigravity.js +5 -9
- package/lib/providers/catalog-store.d.ts +3 -2
- package/lib/providers/catalog-store.js +57 -26
- package/lib/providers/claude.d.ts +22 -3
- package/lib/providers/claude.js +118 -20
- package/lib/providers/codex-search.js +5 -2
- package/lib/providers/codex.d.ts +15 -2
- package/lib/providers/codex.js +77 -9
- package/lib/providers/common.d.ts +38 -0
- package/lib/providers/common.js +44 -18
- package/lib/providers/copilot.d.ts +2 -1
- package/lib/providers/copilot.js +13 -6
- 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.js +2 -1
- package/lib/providers/grok.d.ts +2 -2
- package/lib/providers/grok.js +16 -11
- package/lib/providers/minimax-usage.js +2 -1
- package/lib/providers/pool-health.d.ts +2 -0
- package/lib/providers/pool-health.js +9 -0
- package/lib/providers/pool-usage.d.ts +13 -1
- package/lib/providers/pool-usage.js +39 -7
- package/lib/providers/pool.d.ts +7 -5
- package/lib/providers/pool.js +89 -12
- package/lib/providers/rate-limit.d.ts +0 -14
- package/lib/providers/rate-limit.js +0 -26
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +50 -0
- package/lib/providers/reset-redemption.d.ts +51 -0
- package/lib/providers/reset-redemption.js +102 -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/anthropic.d.ts +21 -2
- package/lib/translate/anthropic.js +35 -11
- package/lib/translate/antigravity.js +13 -6
- package/lib/translate/chat-completions.js +1 -1
- package/lib/translate/resolved.d.ts +24 -0
- package/lib/translate/resolved.js +66 -3
- package/lib/translate/responses.js +8 -6
- package/lib/translate/sse.js +8 -0
- 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) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import { DISCOVERY_TIMEOUT_MS, withTimeout } from './common.js';
|
|
3
|
+
import { streamAccountWithReplay } from './replay.js';
|
|
3
4
|
/** Reserved namespace, recognized even when malformed or no longer enabled. */
|
|
4
5
|
export const ACCOUNT_MODEL_PREFIX = '~account:';
|
|
5
6
|
export function accountModelId(account, model) {
|
|
@@ -140,7 +141,7 @@ export class AccountPreferencesAdapter extends LlmAdapter {
|
|
|
140
141
|
const independent = parseAccountModelId(options.model);
|
|
141
142
|
if (independent) {
|
|
142
143
|
await this.requireAccount(independent.account, independent.model, true);
|
|
143
|
-
yield* this.options.adapter
|
|
144
|
+
yield* streamAccountWithReplay(this.options.adapter, options, independent.account, independent.model);
|
|
144
145
|
return;
|
|
145
146
|
}
|
|
146
147
|
const pool = this.options.pool();
|
|
@@ -148,6 +149,6 @@ export class AccountPreferencesAdapter extends LlmAdapter {
|
|
|
148
149
|
yield* pool.stream(options);
|
|
149
150
|
return;
|
|
150
151
|
}
|
|
151
|
-
yield* this.options.adapter
|
|
152
|
+
yield* streamAccountWithReplay(this.options.adapter, options, await this.fallback(options.model));
|
|
152
153
|
}
|
|
153
154
|
}
|
|
@@ -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
|
}
|
|
@@ -28,8 +28,8 @@ export declare function modelsFilePath(): string;
|
|
|
28
28
|
export declare function sanitizeSnapshot(value: unknown): CatalogSnapshot | undefined;
|
|
29
29
|
/**
|
|
30
30
|
* Build the durable half of one provider's catalog cache over the shared
|
|
31
|
-
* models.json file
|
|
32
|
-
* a
|
|
31
|
+
* models.json file. Saves and clears serialize with all accounts at this path
|
|
32
|
+
* in this process; readers see a complete file before or after an atomic rename.
|
|
33
33
|
* @param provider - the provider route keying the file entry.
|
|
34
34
|
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
35
35
|
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
@@ -40,6 +40,7 @@ export declare function catalogStore(provider: ProviderId, path?: string): Catal
|
|
|
40
40
|
* entry belongs to the default account; every other account keeps its own
|
|
41
41
|
* snapshot under `accounts[provider][account]`, so a working secondary login
|
|
42
42
|
* survives restarts and network failures even when the default login is dead.
|
|
43
|
+
* Saves and clears share {@link catalogStore}'s in-process write chain.
|
|
43
44
|
* @param provider - the provider route.
|
|
44
45
|
* @param account - the canonical account key.
|
|
45
46
|
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* throw on every call, which is worse than having no fallback at all.
|
|
13
13
|
*/
|
|
14
14
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
15
|
-
import { dirname } from 'node:path';
|
|
15
|
+
import { dirname, resolve } from 'node:path';
|
|
16
16
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
17
17
|
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
/**
|
|
@@ -177,10 +177,32 @@ async function writeCatalogFile(store, path) {
|
|
|
177
177
|
throw error;
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Like the auth store, mutations of one resolved path share an in-process
|
|
182
|
+
* write chain in call order. Each reads after the preceding mutation settles;
|
|
183
|
+
* a failure rejects its caller without blocking later mutations. Idle chains
|
|
184
|
+
* are removed. Separate processes (or filesystem aliases of a path) are not
|
|
185
|
+
* locked and remain last-writer-wins.
|
|
186
|
+
*/
|
|
187
|
+
const writeChains = new Map();
|
|
188
|
+
async function serialize(path, action) {
|
|
189
|
+
const key = resolve(path);
|
|
190
|
+
const previous = writeChains.get(key) ?? Promise.resolve();
|
|
191
|
+
const next = previous.then(action, action);
|
|
192
|
+
const tail = next.then(() => undefined, () => undefined);
|
|
193
|
+
writeChains.set(key, tail);
|
|
194
|
+
try {
|
|
195
|
+
return await next;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
if (writeChains.get(key) === tail)
|
|
199
|
+
writeChains.delete(key);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
180
202
|
/**
|
|
181
203
|
* Build the durable half of one provider's catalog cache over the shared
|
|
182
|
-
* models.json file
|
|
183
|
-
* a
|
|
204
|
+
* models.json file. Saves and clears serialize with all accounts at this path
|
|
205
|
+
* in this process; readers see a complete file before or after an atomic rename.
|
|
184
206
|
* @param provider - the provider route keying the file entry.
|
|
185
207
|
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
186
208
|
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
@@ -191,16 +213,20 @@ export function catalogStore(provider, path = modelsFilePath()) {
|
|
|
191
213
|
return sanitizeSnapshot((await readCatalogFile(path))[provider]);
|
|
192
214
|
},
|
|
193
215
|
async save(snapshot) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
216
|
+
return serialize(path, async () => {
|
|
217
|
+
const store = await readCatalogFile(path);
|
|
218
|
+
store[provider] = snapshot;
|
|
219
|
+
await writeCatalogFile(store, path);
|
|
220
|
+
});
|
|
197
221
|
},
|
|
198
222
|
async clear() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
return serialize(path, async () => {
|
|
224
|
+
const store = await readCatalogFile(path);
|
|
225
|
+
if (store[provider] === undefined)
|
|
226
|
+
return;
|
|
227
|
+
delete store[provider];
|
|
228
|
+
await writeCatalogFile(store, path);
|
|
229
|
+
});
|
|
204
230
|
},
|
|
205
231
|
};
|
|
206
232
|
}
|
|
@@ -209,6 +235,7 @@ export function catalogStore(provider, path = modelsFilePath()) {
|
|
|
209
235
|
* entry belongs to the default account; every other account keeps its own
|
|
210
236
|
* snapshot under `accounts[provider][account]`, so a working secondary login
|
|
211
237
|
* survives restarts and network failures even when the default login is dead.
|
|
238
|
+
* Saves and clears share {@link catalogStore}'s in-process write chain.
|
|
212
239
|
* @param provider - the provider route.
|
|
213
240
|
* @param account - the canonical account key.
|
|
214
241
|
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
@@ -221,23 +248,27 @@ export function accountCatalogStore(provider, account, path = modelsFilePath())
|
|
|
221
248
|
return section !== undefined && Object.hasOwn(section, account) ? sanitizeSnapshot(section[account]) : undefined;
|
|
222
249
|
},
|
|
223
250
|
async save(snapshot) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
return serialize(path, async () => {
|
|
252
|
+
const store = await readCatalogFile(path);
|
|
253
|
+
const accounts = typeof store.accounts === 'object' && store.accounts !== null && !Array.isArray(store.accounts)
|
|
254
|
+
? store.accounts
|
|
255
|
+
: {};
|
|
256
|
+
const section = { ...accountSection(store, provider) ?? {}, [account]: snapshot };
|
|
257
|
+
store.accounts = { ...accounts, [provider]: section };
|
|
258
|
+
await writeCatalogFile(store, path);
|
|
259
|
+
});
|
|
231
260
|
},
|
|
232
261
|
async clear() {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
return serialize(path, async () => {
|
|
263
|
+
const store = await readCatalogFile(path);
|
|
264
|
+
const section = accountSection(store, provider);
|
|
265
|
+
if (section === undefined || !Object.hasOwn(section, account))
|
|
266
|
+
return;
|
|
267
|
+
const rest = { ...section };
|
|
268
|
+
delete rest[account];
|
|
269
|
+
store.accounts = { ...store.accounts, [provider]: rest };
|
|
270
|
+
await writeCatalogFile(store, path);
|
|
271
|
+
});
|
|
241
272
|
},
|
|
242
273
|
};
|
|
243
274
|
}
|
|
@@ -10,6 +10,7 @@ import type { ClaudeSession } from '../auth/store.js';
|
|
|
10
10
|
import type { PoolAdapter } from './pool.js';
|
|
11
11
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
12
12
|
import type { TranslatableMessage } from '../translate/resolved.js';
|
|
13
|
+
import type { PromptCacheTtl } from '../translate/anthropic.js';
|
|
13
14
|
import { AccountTokenManager } from './accounts.js';
|
|
14
15
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
15
16
|
import type { CliVersion } from './npm-cli-version.js';
|
|
@@ -34,8 +35,8 @@ export declare const CLAUDE_PREEMPT_MS: number;
|
|
|
34
35
|
* headers are deliberately not read: they are rollover snapshots attached to
|
|
35
36
|
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
36
37
|
* earliest of them is typically the bucket that still had room — a wait that
|
|
37
|
-
* lands straight back in the closed window.
|
|
38
|
-
*
|
|
38
|
+
* lands straight back in the closed window. The parsed reset supplies the retry
|
|
39
|
+
* hint; diagnostics omit raw provider headers and body text.
|
|
39
40
|
*/
|
|
40
41
|
export declare const claudeRateLimitReset: RateLimitResetReader;
|
|
41
42
|
/**
|
|
@@ -86,6 +87,10 @@ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usa
|
|
|
86
87
|
* source of Claude Code's `/usage` screen). Newer responses carry a
|
|
87
88
|
* structured `limits` array; older ones the flat `five_hour`/`seven_day*`
|
|
88
89
|
* buckets — both shapes are read, the array winning when it has entries.
|
|
90
|
+
* Requests the optional cedar_ember banked-reset block with the existing CLI
|
|
91
|
+
* identity. A 400/403 retries once without that parameter; ordinary windows
|
|
92
|
+
* remain available and resetCreditsError records the optional lookup failure.
|
|
93
|
+
* Missing/ineligible blocks and empty or unusable grants expose no credits.
|
|
89
94
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
90
95
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
91
96
|
* @param signal - caller cancellation from the RPC transport.
|
|
@@ -124,7 +129,17 @@ export interface ClaudeAdapterOptions {
|
|
|
124
129
|
defaultEffortOf?: (model: string) => string | undefined;
|
|
125
130
|
/** Resolves the Claude Code version to present (npm-backed); absent means the local floor. */
|
|
126
131
|
resolveCliVersion?: () => Promise<string>;
|
|
132
|
+
/** Cache breakpoint lifetime; absent means the API's five-minute default. */
|
|
133
|
+
promptCacheTtl?: PromptCacheTtl;
|
|
127
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Inline image data a Claude request may carry, in base64 bytes. Every turn
|
|
137
|
+
* resends the history, so screenshots accumulate until the request passes the
|
|
138
|
+
* API's 32MB body limit and every later turn and compaction fails with HTTP
|
|
139
|
+
* 413 `request_too_large`. Past this budget the host offloads the oldest
|
|
140
|
+
* images to text and retries; the remaining 12MB holds prompts, tools, and text.
|
|
141
|
+
*/
|
|
142
|
+
export declare const CLAUDE_REQUEST_IMAGE_BUDGET: number;
|
|
128
143
|
/**
|
|
129
144
|
* Assemble the Anthropic request body.
|
|
130
145
|
*
|
|
@@ -138,9 +153,13 @@ export interface ClaudeAdapterOptions {
|
|
|
138
153
|
* @param maxTokens - the resolved output cap.
|
|
139
154
|
* @param thinking - the thinking parameter, when the model takes one.
|
|
140
155
|
* @param effort - the reasoning effort, when the model advertises efforts.
|
|
156
|
+
* @param cacheTtl - lifetime of every cache breakpoint; defaults to five minutes.
|
|
157
|
+
* Requests that carry a `purpose` (compaction, session titles) always use five
|
|
158
|
+
* minutes to preserve their existing auxiliary-request behavior. A five-minute
|
|
159
|
+
* mark can still read an entry a one-hour mark wrote.
|
|
141
160
|
* @returns the JSON body to POST.
|
|
142
161
|
*/
|
|
143
|
-
export declare function claudeRequestBody(options: GenerateOptions, messages: readonly TranslatableMessage[], maxTokens: number, thinking?: Record<string, unknown>, effort?: string): Record<string, unknown>;
|
|
162
|
+
export declare function claudeRequestBody(options: GenerateOptions, messages: readonly TranslatableMessage[], maxTokens: number, thinking?: Record<string, unknown>, effort?: string, cacheTtl?: PromptCacheTtl): Record<string, unknown>;
|
|
144
163
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
145
164
|
export declare class ClaudeAdapter extends LlmAdapter {
|
|
146
165
|
private readonly options;
|
package/lib/providers/claude.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
8
|
-
import { resolveImages } from '../translate/resolved.js';
|
|
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';
|
|
@@ -40,8 +40,8 @@ const CLAUDE_RESET_FIELDS = ['resets_at', 'resetsAt', 'reset_at', 'retry_after']
|
|
|
40
40
|
* headers are deliberately not read: they are rollover snapshots attached to
|
|
41
41
|
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
42
42
|
* earliest of them is typically the bucket that still had room — a wait that
|
|
43
|
-
* lands straight back in the closed window.
|
|
44
|
-
*
|
|
43
|
+
* lands straight back in the closed window. The parsed reset supplies the retry
|
|
44
|
+
* hint; diagnostics omit raw provider headers and body text.
|
|
45
45
|
*/
|
|
46
46
|
export const claudeRateLimitReset = (response, body, now) => {
|
|
47
47
|
const unified = earliestReset(resetInstantFromHeader(response, 'anthropic-ratelimit-unified-reset', now), resetInstantFromHeader(response, 'anthropic-ratelimit-unified-fallback-reset', now));
|
|
@@ -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 },
|
|
@@ -249,12 +249,70 @@ export function isClaudePermanentRefreshError(error) {
|
|
|
249
249
|
&& (error.oauthCode === 'invalid_grant' || error.oauthCode === 'invalid_token');
|
|
250
250
|
}
|
|
251
251
|
export const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
252
|
-
/**
|
|
252
|
+
/** ISO-8601 or numeric epoch (seconds below 1e10 in magnitude, otherwise ms). */
|
|
253
253
|
function claudeResetsAt(value) {
|
|
254
|
-
|
|
254
|
+
let parsed;
|
|
255
|
+
if (typeof value === 'number') {
|
|
256
|
+
parsed = Math.trunc(Math.abs(value) < 1e10 ? value * 1000 : value);
|
|
257
|
+
}
|
|
258
|
+
else if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
259
|
+
parsed = Date.parse(value);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
255
262
|
return undefined;
|
|
256
|
-
|
|
257
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
263
|
+
}
|
|
264
|
+
return Number.isFinite(parsed) && Number.isFinite(new Date(parsed).getTime()) ? parsed : undefined;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Read the observed cedar_ember shape from ItsJazii/pane, not an official
|
|
268
|
+
* contract. Each entry retains its grant id and counts; claimable is only
|
|
269
|
+
* read-only server-selection metadata, never an implemented redemption path.
|
|
270
|
+
*/
|
|
271
|
+
function claudeResetCredits(value, now) {
|
|
272
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
273
|
+
return undefined;
|
|
274
|
+
const ember = value;
|
|
275
|
+
if (ember.eligible !== true || !Array.isArray(ember.grants))
|
|
276
|
+
return undefined;
|
|
277
|
+
const cooldownUntil = claudeResetsAt(ember.cooldown_until);
|
|
278
|
+
// An unparseable disclosed cooldown must not assert that claims are available.
|
|
279
|
+
const cooling = cooldownUntil === undefined
|
|
280
|
+
? ember.cooldown_until !== undefined && ember.cooldown_until !== null
|
|
281
|
+
: cooldownUntil > now;
|
|
282
|
+
const credits = [];
|
|
283
|
+
let selected = false;
|
|
284
|
+
for (const raw of ember.grants) {
|
|
285
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
286
|
+
continue;
|
|
287
|
+
const grant = raw;
|
|
288
|
+
const { id, resets_total: total, resets_left: left, usable_now: usable, paused } = grant;
|
|
289
|
+
const expiresAt = claudeResetsAt(grant.ends_at);
|
|
290
|
+
if (typeof id !== 'string' || id.trim().length === 0
|
|
291
|
+
|| typeof total !== 'number' || !Number.isSafeInteger(total) || total < 1
|
|
292
|
+
|| typeof left !== 'number' || !Number.isSafeInteger(left)
|
|
293
|
+
|| expiresAt === undefined
|
|
294
|
+
|| (usable !== undefined && typeof usable !== 'boolean')
|
|
295
|
+
|| (paused !== undefined && typeof paused !== 'boolean'))
|
|
296
|
+
continue;
|
|
297
|
+
const resetsLeft = Math.max(0, Math.min(left, total));
|
|
298
|
+
if (resetsLeft === 0)
|
|
299
|
+
continue;
|
|
300
|
+
const claimable = !selected && id === ember.next_grant_id && usable === true
|
|
301
|
+
&& paused !== true && !cooling && expiresAt > now;
|
|
302
|
+
if (claimable)
|
|
303
|
+
selected = true;
|
|
304
|
+
credits.push({
|
|
305
|
+
id,
|
|
306
|
+
expiresAt,
|
|
307
|
+
resetsTotal: total,
|
|
308
|
+
resetsLeft,
|
|
309
|
+
...usable === undefined ? {} : { usableNow: usable },
|
|
310
|
+
...paused === undefined ? {} : { paused },
|
|
311
|
+
claimable,
|
|
312
|
+
...cooldownUntil === undefined ? {} : { cooldownUntil },
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return credits.length > 0 ? credits : undefined;
|
|
258
316
|
}
|
|
259
317
|
/** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
|
|
260
318
|
function claudeWindowTiming(kind) {
|
|
@@ -308,6 +366,10 @@ function claudeLimitsWindows(value) {
|
|
|
308
366
|
* source of Claude Code's `/usage` screen). Newer responses carry a
|
|
309
367
|
* structured `limits` array; older ones the flat `five_hour`/`seven_day*`
|
|
310
368
|
* buckets — both shapes are read, the array winning when it has entries.
|
|
369
|
+
* Requests the optional cedar_ember banked-reset block with the existing CLI
|
|
370
|
+
* identity. A 400/403 retries once without that parameter; ordinary windows
|
|
371
|
+
* remain available and resetCreditsError records the optional lookup failure.
|
|
372
|
+
* Missing/ineligible blocks and empty or unusable grants expose no credits.
|
|
311
373
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
312
374
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
313
375
|
* @param signal - caller cancellation from the RPC transport.
|
|
@@ -316,7 +378,7 @@ function claudeLimitsWindows(value) {
|
|
|
316
378
|
*/
|
|
317
379
|
export async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion = localClaudeCliVersion) {
|
|
318
380
|
const userAgent = claudeCliUserAgent(await cliVersion());
|
|
319
|
-
const
|
|
381
|
+
const init = {
|
|
320
382
|
headers: {
|
|
321
383
|
'authorization': `Bearer ${session.accessToken}`,
|
|
322
384
|
'anthropic-beta': 'oauth-2025-04-20',
|
|
@@ -326,13 +388,25 @@ export async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cli
|
|
|
326
388
|
'accept': 'application/json',
|
|
327
389
|
},
|
|
328
390
|
...signal === undefined ? {} : { signal },
|
|
329
|
-
}
|
|
391
|
+
};
|
|
392
|
+
let response = await fetchFn(`${CLAUDE_USAGE_URL}?cedar_ember=1`, init);
|
|
393
|
+
let resetCreditsError;
|
|
394
|
+
if (response.status === 400 || response.status === 403) {
|
|
395
|
+
resetCreditsError = `Claude banked resets lookup failed (HTTP ${response.status}); using plain usage.`;
|
|
396
|
+
// Optional program support must not turn a working usage endpoint into an error.
|
|
397
|
+
response = await fetchFn(CLAUDE_USAGE_URL, init);
|
|
398
|
+
}
|
|
330
399
|
if (!response.ok)
|
|
331
400
|
throw await oauthEndpointError(response, 'claude usage');
|
|
332
|
-
const payload = await response
|
|
401
|
+
const payload = await parseProviderJson(response, 'claude usage');
|
|
402
|
+
const resetCredits = resetCreditsError === undefined ? claudeResetCredits(payload.cedar_ember, Date.now()) : undefined;
|
|
403
|
+
const resets = {
|
|
404
|
+
...resetCredits === undefined ? {} : { resetCredits },
|
|
405
|
+
...resetCreditsError === undefined ? {} : { resetCreditsError },
|
|
406
|
+
};
|
|
333
407
|
const modern = claudeLimitsWindows(payload.limits);
|
|
334
408
|
if (modern.length > 0)
|
|
335
|
-
return { supported: true, windows: modern };
|
|
409
|
+
return { supported: true, windows: modern, ...resets };
|
|
336
410
|
const windows = [];
|
|
337
411
|
const legacy = [
|
|
338
412
|
claudeLegacyWindow(payload.five_hour, 'session'),
|
|
@@ -344,7 +418,7 @@ export async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cli
|
|
|
344
418
|
if (window !== undefined)
|
|
345
419
|
windows.push(window);
|
|
346
420
|
}
|
|
347
|
-
return { supported: true, windows };
|
|
421
|
+
return { supported: true, windows, ...resets };
|
|
348
422
|
}
|
|
349
423
|
function claudeThinkingType(capabilities) {
|
|
350
424
|
const types = capabilities?.thinking?.types;
|
|
@@ -388,7 +462,7 @@ export async function fetchClaudeModels(session, fetchFn = hostFetch, signal, cl
|
|
|
388
462
|
});
|
|
389
463
|
if (!response.ok)
|
|
390
464
|
throw await httpLlmError(response, 'claude models API');
|
|
391
|
-
const payload = await response
|
|
465
|
+
const payload = await parseProviderJson(response, 'claude models API');
|
|
392
466
|
if (!Array.isArray(payload.data)) {
|
|
393
467
|
throw new Error('claude models API returned an invalid catalog');
|
|
394
468
|
}
|
|
@@ -431,6 +505,14 @@ function claudeMaxTokens(configured, disc) {
|
|
|
431
505
|
* this cap; 3.75MB encoded stays under the 5MB base64 per-image limit.
|
|
432
506
|
*/
|
|
433
507
|
const CLAUDE_IMAGE_LIMIT = { maxEdge: 2000, maxBytes: 3_750_000 };
|
|
508
|
+
/**
|
|
509
|
+
* Inline image data a Claude request may carry, in base64 bytes. Every turn
|
|
510
|
+
* resends the history, so screenshots accumulate until the request passes the
|
|
511
|
+
* API's 32MB body limit and every later turn and compaction fails with HTTP
|
|
512
|
+
* 413 `request_too_large`. Past this budget the host offloads the oldest
|
|
513
|
+
* images to text and retries; the remaining 12MB holds prompts, tools, and text.
|
|
514
|
+
*/
|
|
515
|
+
export const CLAUDE_REQUEST_IMAGE_BUDGET = 20 * 1024 * 1024;
|
|
434
516
|
/** The Claude 4.5 family accepts image input. */
|
|
435
517
|
const CLAUDE_MODALITIES = ['text', 'image'];
|
|
436
518
|
/**
|
|
@@ -446,15 +528,23 @@ const CLAUDE_MODALITIES = ['text', 'image'];
|
|
|
446
528
|
* @param maxTokens - the resolved output cap.
|
|
447
529
|
* @param thinking - the thinking parameter, when the model takes one.
|
|
448
530
|
* @param effort - the reasoning effort, when the model advertises efforts.
|
|
531
|
+
* @param cacheTtl - lifetime of every cache breakpoint; defaults to five minutes.
|
|
532
|
+
* Requests that carry a `purpose` (compaction, session titles) always use five
|
|
533
|
+
* minutes to preserve their existing auxiliary-request behavior. A five-minute
|
|
534
|
+
* mark can still read an entry a one-hour mark wrote.
|
|
449
535
|
* @returns the JSON body to POST.
|
|
450
536
|
*/
|
|
451
|
-
export function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
|
|
537
|
+
export function claudeRequestBody(options, messages, maxTokens, thinking, effort, cacheTtl) {
|
|
538
|
+
// Read structurally so older host types need not declare `purpose`. Hosts
|
|
539
|
+
// that do not classify auxiliary calls follow the configured TTL.
|
|
540
|
+
const purpose = options.purpose;
|
|
541
|
+
const ttl = purpose === undefined ? cacheTtl : undefined;
|
|
452
542
|
const anthropicMessages = toAnthropicMessages(messages);
|
|
453
|
-
markMessageCache(anthropicMessages);
|
|
543
|
+
markMessageCache(anthropicMessages, ttl);
|
|
454
544
|
return {
|
|
455
545
|
model: options.model,
|
|
456
546
|
max_tokens: maxTokens,
|
|
457
|
-
system: toAnthropicSystem(options.system, messages),
|
|
547
|
+
system: toAnthropicSystem(options.system, messages, ttl),
|
|
458
548
|
messages: anthropicMessages,
|
|
459
549
|
...options.tools !== undefined && options.tools.length > 0
|
|
460
550
|
? { tools: toAnthropicTools(options.tools) }
|
|
@@ -659,6 +749,14 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
659
749
|
}
|
|
660
750
|
async request(options, session, signal) {
|
|
661
751
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal, CLAUDE_IMAGE_LIMIT);
|
|
752
|
+
if (hostSupportsImageOffload()) {
|
|
753
|
+
const offloadImages = requiredImageOffloadCount(messages, CLAUDE_REQUEST_IMAGE_BUDGET);
|
|
754
|
+
if (offloadImages > 0) {
|
|
755
|
+
// The supported 0.1.7-rc.2 floor reads `offloadImages`; older option types lack it.
|
|
756
|
+
throw new LlmError(`claude request images exceed the ${CLAUDE_REQUEST_IMAGE_BUDGET}-byte base64 budget; `
|
|
757
|
+
+ `${offloadImages} more oldest image(s) must be offloaded`, IMAGE_OFFLOAD_REQUIRED, { offloadImages });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
662
760
|
const disc = await this.discovered(options.model);
|
|
663
761
|
const maxTokens = options.maxTokens
|
|
664
762
|
?? claudeMaxTokens(this.options.models.find(entry => entry.id === options.model), disc);
|
|
@@ -666,7 +764,7 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
666
764
|
const effort = options.reasoningEffort !== undefined && disc?.reasoning !== undefined
|
|
667
765
|
? String(options.reasoningEffort)
|
|
668
766
|
: undefined;
|
|
669
|
-
const body = claudeRequestBody(options, messages, maxTokens, thinking, effort);
|
|
767
|
+
const body = claudeRequestBody(options, messages, maxTokens, thinking, effort, this.options.promptCacheTtl);
|
|
670
768
|
const cliVersion = await (this.options.resolveCliVersion ?? localClaudeCliVersion)();
|
|
671
769
|
return hostFetch(CLAUDE_API_URL, {
|
|
672
770
|
method: 'POST',
|