dsh-subscriptions 0.1.2 → 0.1.4
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 +92 -10
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- package/lib/client/ExternalUsageCards.d.ts +11 -1
- package/lib/client/ExternalUsageCards.js +18 -3
- 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.d.ts +19 -2
- package/lib/client/SubscriptionUsageBadge.js +65 -9
- package/lib/client/SubscriptionsSection.d.ts +22 -0
- package/lib/client/SubscriptionsSection.js +128 -38
- package/lib/client/UsageBadgeDisplaySetting.js +6 -10
- package/lib/client/UsageMeter.js +11 -6
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +60 -0
- package/lib/client/locales.js +62 -2
- 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/usage-badge-preferences.d.ts +7 -2
- package/lib/client/usage-badge-preferences.js +3 -2
- package/lib/client/usage-pace.d.ts +14 -1
- package/lib/client/usage-pace.js +14 -1
- package/lib/client.js +1036 -334
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +1120 -602
- package/lib/providers/account-preferences.js +3 -2
- 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 +113 -15
- package/lib/providers/codex.d.ts +15 -2
- package/lib/providers/codex.js +72 -4
- package/lib/providers/common.d.ts +25 -0
- package/lib/providers/common.js +22 -18
- package/lib/providers/copilot.d.ts +2 -1
- package/lib/providers/copilot.js +10 -3
- package/lib/providers/grok.d.ts +2 -2
- package/lib/providers/grok.js +2 -2
- package/lib/providers/minimax-usage.d.ts +22 -1
- package/lib/providers/minimax-usage.js +37 -7
- 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/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 +21 -0
- package/lib/translate/resolved.js +63 -3
- package/lib/translate/responses.js +8 -6
- package/lib/translate/sse.js +8 -0
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -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,7 +5,7 @@
|
|
|
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
10
|
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
11
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.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));
|
|
@@ -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
401
|
const payload = await response.json();
|
|
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;
|
|
@@ -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
|
+
// Older dsh-llm option types lack `offloadImages`; 0.2+ hosts read 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',
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const CODEX_PREEMPT_MS: number;
|
|
|
26
26
|
* rollover snapshots the backend attaches to every response, one per window,
|
|
27
27
|
* so they say nothing about which window refused: a burst 429 that would clear
|
|
28
28
|
* in seconds still carries a primary rollover hours out, and reading it would
|
|
29
|
-
* park the turn for those hours.
|
|
30
|
-
*
|
|
29
|
+
* park the turn for those hours. The parsed body reset supplies the retry hint;
|
|
30
|
+
* diagnostics omit raw provider headers and body text.
|
|
31
31
|
*/
|
|
32
32
|
export declare const codexRateLimitReset: RateLimitResetReader;
|
|
33
33
|
/**
|
|
@@ -77,6 +77,11 @@ export declare function refreshCodex(session: CodexSession): Promise<CodexSessio
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function isCodexPermanentRefreshError(error: unknown): boolean;
|
|
79
79
|
export declare const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
80
|
+
export declare const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
81
|
+
/** Read banked resets without spending them; consumption uses a separate explicit POST. */
|
|
82
|
+
export declare function fetchCodexResetCredits(session: CodexSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<NonNullable<ProviderUsage['resetCredits']>>;
|
|
83
|
+
/** Submit one explicit reset; ambiguous or unconfirmed responses throw without retry. */
|
|
84
|
+
export declare function consumeCodexResetCredit(session: CodexSession, creditId: string, requestId: string, fetchFn?: FetchFn, signal?: AbortSignal): Promise<void>;
|
|
80
85
|
/**
|
|
81
86
|
* Fetch the codex subscription usage from the ChatGPT backend wham/usage
|
|
82
87
|
* endpoint (the source of the codex CLI `/status` rate-limit lines). The
|
|
@@ -164,6 +169,14 @@ export declare const CODEX_UNKNOWN_TOOL_OUTCOME = "The tool call has no recorded
|
|
|
164
169
|
* @returns the same array when already balanced, otherwise a repaired array.
|
|
165
170
|
*/
|
|
166
171
|
export declare function reconcileResponsesToolCalls(input: ResponsesRequestInput['input']): ResponsesRequestInput['input'];
|
|
172
|
+
/**
|
|
173
|
+
* The `reasoning.effort` wire value for a selected effort. Codex's "ultra" is
|
|
174
|
+
* a client-side selection (max reasoning plus proactive sub-agent delegation)
|
|
175
|
+
* that the Codex CLI itself lowers to `max` at the Responses API boundary;
|
|
176
|
+
* the backend answers HTTP 400 to a literal `ultra`. Discovery no longer
|
|
177
|
+
* lists it, but an effort saved before that change may still arrive here.
|
|
178
|
+
*/
|
|
179
|
+
export declare function codexWireEffort(effort: string): string;
|
|
167
180
|
/**
|
|
168
181
|
* The Responses request body for one generation. A fast-tier request (the
|
|
169
182
|
* composer Speed toggle, the codex CLI's fast mode) carries
|
package/lib/providers/codex.js
CHANGED
|
@@ -35,8 +35,8 @@ const CODEX_RESET_FIELDS = ['resets_in_seconds', 'reset_after_seconds', 'resets_
|
|
|
35
35
|
* rollover snapshots the backend attaches to every response, one per window,
|
|
36
36
|
* so they say nothing about which window refused: a burst 429 that would clear
|
|
37
37
|
* in seconds still carries a primary rollover hours out, and reading it would
|
|
38
|
-
* park the turn for those hours.
|
|
39
|
-
*
|
|
38
|
+
* park the turn for those hours. The parsed body reset supplies the retry hint;
|
|
39
|
+
* diagnostics omit raw provider headers and body text.
|
|
40
40
|
*/
|
|
41
41
|
export const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
42
42
|
/** Default instruction when the request carries no system prompt. */
|
|
@@ -217,6 +217,62 @@ export function isCodexPermanentRefreshError(error) {
|
|
|
217
217
|
&& PERMANENT_REFRESH_CODES.has(error.oauthCode);
|
|
218
218
|
}
|
|
219
219
|
export const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
220
|
+
export const CODEX_RESET_CREDITS_URL = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits';
|
|
221
|
+
function parseResetDate(value) {
|
|
222
|
+
if (typeof value !== 'string')
|
|
223
|
+
return undefined;
|
|
224
|
+
const timestamp = Date.parse(value);
|
|
225
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
226
|
+
}
|
|
227
|
+
/** Read banked resets without spending them; consumption uses a separate explicit POST. */
|
|
228
|
+
export async function fetchCodexResetCredits(session, fetchFn = hostFetch, signal) {
|
|
229
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_URL, {
|
|
230
|
+
headers: {
|
|
231
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
232
|
+
'chatgpt-account-id': session.accountId,
|
|
233
|
+
originator: 'Codex Desktop',
|
|
234
|
+
'openai-beta': 'codex-1',
|
|
235
|
+
accept: 'application/json',
|
|
236
|
+
...attributionHeaders(),
|
|
237
|
+
},
|
|
238
|
+
...signal === undefined ? {} : { signal },
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok)
|
|
241
|
+
throw await oauthEndpointError(response, 'codex reset credits');
|
|
242
|
+
const payload = await response.json();
|
|
243
|
+
if (!Array.isArray(payload?.credits))
|
|
244
|
+
throw new Error('codex reset credits: unexpected response (missing credits array)');
|
|
245
|
+
return payload.credits.flatMap((value) => {
|
|
246
|
+
if (typeof value !== 'object' || value === null)
|
|
247
|
+
return [];
|
|
248
|
+
const credit = value;
|
|
249
|
+
if (credit.status !== 'available' || credit.reset_type !== 'codex_rate_limits')
|
|
250
|
+
return [];
|
|
251
|
+
const grantedAt = parseResetDate(credit.granted_at);
|
|
252
|
+
const expiresAt = parseResetDate(credit.expires_at);
|
|
253
|
+
return [{ ...(typeof credit.id === 'string' ? { id: credit.id } : {}), ...(grantedAt === undefined ? {} : { grantedAt }), ...(expiresAt === undefined ? {} : { expiresAt }) }];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Submit one explicit reset; ambiguous or unconfirmed responses throw without retry. */
|
|
257
|
+
export async function consumeCodexResetCredit(session, creditId, requestId, fetchFn = hostFetch, signal) {
|
|
258
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_URL + '/consume', {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
authorization: 'Bearer ' + session.accessToken,
|
|
262
|
+
'chatgpt-account-id': session.accountId,
|
|
263
|
+
originator: 'Codex Desktop', 'openai-beta': 'codex-1',
|
|
264
|
+
'content-type': 'application/json', accept: 'application/json',
|
|
265
|
+
...attributionHeaders(),
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify({ redeem_request_id: requestId, credit_id: creditId }),
|
|
268
|
+
...signal === undefined ? {} : { signal },
|
|
269
|
+
});
|
|
270
|
+
if (!response.ok)
|
|
271
|
+
throw await oauthEndpointError(response, 'codex consume reset');
|
|
272
|
+
const payload = await response.json();
|
|
273
|
+
if (payload?.code !== 'reset')
|
|
274
|
+
throw new Error('Reset was not confirmed. Verify the account in Codex before retrying.');
|
|
275
|
+
}
|
|
220
276
|
/** Seconds of the canonical 5-hour session and 7-day weekly windows. */
|
|
221
277
|
const SESSION_WINDOW_SECONDS = 5 * 60 * 60;
|
|
222
278
|
const WEEKLY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
|
|
@@ -365,8 +421,10 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
|
|
|
365
421
|
// dropped, and an absent or unknown value is included (in doubt, include).
|
|
366
422
|
if (entry.visibility === 'hide' || entry.visibility === 'none')
|
|
367
423
|
continue;
|
|
424
|
+
// "ultra" is a Codex-client mode (max reasoning + automatic sub-agent
|
|
425
|
+
// delegation), not a wire value: the Responses API rejects it with HTTP 400.
|
|
368
426
|
const efforts = (entry.supported_reasoning_levels ?? [])
|
|
369
|
-
.filter(level => typeof level.effort === 'string' && level.effort.length > 0)
|
|
427
|
+
.filter(level => typeof level.effort === 'string' && level.effort.length > 0 && level.effort !== 'ultra')
|
|
370
428
|
.map(level => ({
|
|
371
429
|
id: ReasoningEffortId(level.effort),
|
|
372
430
|
name: effortDisplayName(level.effort),
|
|
@@ -491,6 +549,16 @@ export function reconcileResponsesToolCalls(input) {
|
|
|
491
549
|
}
|
|
492
550
|
return balanced;
|
|
493
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* The `reasoning.effort` wire value for a selected effort. Codex's "ultra" is
|
|
554
|
+
* a client-side selection (max reasoning plus proactive sub-agent delegation)
|
|
555
|
+
* that the Codex CLI itself lowers to `max` at the Responses API boundary;
|
|
556
|
+
* the backend answers HTTP 400 to a literal `ultra`. Discovery no longer
|
|
557
|
+
* lists it, but an effort saved before that change may still arrive here.
|
|
558
|
+
*/
|
|
559
|
+
export function codexWireEffort(effort) {
|
|
560
|
+
return effort === 'ultra' ? 'max' : effort;
|
|
561
|
+
}
|
|
494
562
|
/**
|
|
495
563
|
* The Responses request body for one generation. A fast-tier request (the
|
|
496
564
|
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
@@ -509,7 +577,7 @@ export function codexRequestBody(options, resolved, fast) {
|
|
|
509
577
|
? { tools: toResponsesTools(options.tools, { strict: false }), tool_choice: 'auto', parallel_tool_calls: true }
|
|
510
578
|
: {},
|
|
511
579
|
...options.reasoningEffort !== undefined
|
|
512
|
-
? { reasoning: { effort: String(options.reasoningEffort), summary: 'auto' } }
|
|
580
|
+
? { reasoning: { effort: codexWireEffort(String(options.reasoningEffort)), summary: 'auto' } }
|
|
513
581
|
: {},
|
|
514
582
|
store: false,
|
|
515
583
|
stream: true,
|
|
@@ -200,6 +200,27 @@ export interface UsageWindow {
|
|
|
200
200
|
/** Provider-verified fixed interval; permits deriving start from end and duration. */
|
|
201
201
|
fixedWindow?: boolean;
|
|
202
202
|
}
|
|
203
|
+
/** One banked reset credit (Codex), or one grant containing resets (Claude). */
|
|
204
|
+
export interface ResetCredit {
|
|
205
|
+
id?: string;
|
|
206
|
+
grantedAt?: number;
|
|
207
|
+
/** Expiration in epoch ms; never a recurring usage-window reset. */
|
|
208
|
+
expiresAt?: number;
|
|
209
|
+
/** Claude grant counts; one array entry per grant, not per remaining reset. */
|
|
210
|
+
resetsTotal?: number;
|
|
211
|
+
resetsLeft?: number;
|
|
212
|
+
/** Claude's disclosed grant state, absent when the server omits it. */
|
|
213
|
+
usableNow?: boolean;
|
|
214
|
+
paused?: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Claude server-selected grant is currently usable. Read-only metadata, not
|
|
217
|
+
* permission to redeem; no Claude redemption endpoint is implemented.
|
|
218
|
+
* Codex entries omit this field and retain their existing semantics.
|
|
219
|
+
*/
|
|
220
|
+
claimable?: boolean;
|
|
221
|
+
/** Claude program cooldown in epoch ms, repeated on each grant when disclosed. */
|
|
222
|
+
cooldownUntil?: number;
|
|
223
|
+
}
|
|
203
224
|
/** Subscription usage of one provider, as served by the `usage` RPC endpoint. */
|
|
204
225
|
export interface ProviderUsage {
|
|
205
226
|
/** Original successful observation time, preserved across cache reads. */
|
|
@@ -212,6 +233,10 @@ export interface ProviderUsage {
|
|
|
212
233
|
windows?: UsageWindow[];
|
|
213
234
|
/** Plan name the usage endpoint reported, when present. */
|
|
214
235
|
plan?: string;
|
|
236
|
+
/** Banked Codex resets or Claude grants, when the provider discloses them. */
|
|
237
|
+
resetCredits?: ResetCredit[];
|
|
238
|
+
/** Optional reset lookup failure; ordinary usage remains usable. */
|
|
239
|
+
resetCreditsError?: string;
|
|
215
240
|
}
|
|
216
241
|
/** One model discovered from a provider's live model-list endpoint. */
|
|
217
242
|
export interface DiscoveredModel {
|