dsh-plugin-subscriptions 0.5.3 → 0.6.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 +45 -6
- package/README.zh.md +43 -4
- package/lib/auth/rpc.d.ts +36 -2
- package/lib/auth/rpc.js +47 -5
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/SpeedSelect.d.ts +25 -2
- package/lib/client/SpeedSelect.js +10 -6
- package/lib/client/SubscriptionsSection.d.ts +74 -0
- package/lib/client/SubscriptionsSection.js +325 -4
- package/lib/client/VideoGenerateToolview.d.ts +1 -1
- package/lib/client/index.d.ts +1 -9
- package/lib/client/index.js +7 -4
- package/lib/client/locales.d.ts +28 -0
- package/lib/client/locales.js +28 -0
- package/lib/client.js +458 -10
- package/lib/client.js.map +1 -1
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +865 -111
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/providers/claude.d.ts +24 -3
- package/lib/providers/claude.js +35 -24
- package/lib/providers/codex.d.ts +21 -0
- package/lib/providers/codex.js +37 -10
- package/lib/providers/common.d.ts +70 -6
- package/lib/providers/common.js +118 -19
- package/lib/providers/copilot.d.ts +10 -0
- package/lib/providers/copilot.js +21 -8
- package/lib/providers/grok.d.ts +21 -0
- package/lib/providers/grok.js +37 -7
- package/lib/providers/pool-usage.d.ts +23 -2
- package/lib/providers/pool-usage.js +70 -15
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/translate/anthropic.js +5 -4
- package/lib/translate/chat-completions.js +5 -4
- package/lib/translate/responses.js +5 -4
- package/package.json +21 -21
- package/lib/providers/antigravity.d.ts +0 -90
- package/lib/providers/antigravity.js +0 -392
- package/lib/translate/antigravity.d.ts +0 -110
- package/lib/translate/antigravity.js +0 -303
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ProviderId } from './auth/store.js';
|
|
2
|
+
/** One model id → its configured default reasoning effort id. */
|
|
3
|
+
export type ModelDefaultMap = Readonly<Record<string, string>>;
|
|
4
|
+
/** Provider route → model defaults. */
|
|
5
|
+
export type ModelDefaults = Readonly<Partial<Record<ProviderId, ModelDefaultMap>>>;
|
|
6
|
+
/** Absolute path of the defaults file. */
|
|
7
|
+
export declare function modelDefaultsFilePath(): string;
|
|
8
|
+
/**
|
|
9
|
+
* The last load failure, or a warning about entries that were skipped while
|
|
10
|
+
* loading; consumers only use it for diagnostics.
|
|
11
|
+
*/
|
|
12
|
+
export declare function modelDefaultsLoadError(): unknown;
|
|
13
|
+
/** A detached snapshot for the RPC surface (render + diffing). */
|
|
14
|
+
export declare function modelDefaultsSnapshot(): ModelDefaults;
|
|
15
|
+
/**
|
|
16
|
+
* Set or clear one model's configured default effort, then persist. The
|
|
17
|
+
* memory snapshot updates only after the atomic write succeeds, so a failed
|
|
18
|
+
* write never leaves the live state ahead of the file.
|
|
19
|
+
* @param provider - the subscription provider route.
|
|
20
|
+
* @param model - the wire model id.
|
|
21
|
+
* @param effort - the effort id, or undefined to clear the override.
|
|
22
|
+
*/
|
|
23
|
+
export declare function setDefaultEffort(provider: ProviderId, model: string, effort: string | undefined): Promise<void>;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-model default reasoning effort overrides — the durable half of the
|
|
3
|
+
* Settings page's per-model "default effort" pickers.
|
|
4
|
+
*
|
|
5
|
+
* The file lives at `~/.dsh/plugins/subscriptions/model-defaults.json`
|
|
6
|
+
* (mode 0600, atomic replace). Shape: `{ "<provider>": { "<model id>": "<effort>" } }`.
|
|
7
|
+
* An absent entry means "follow the provider's own default": the `Default`
|
|
8
|
+
* chip the model picker shows when the discovered catalog advertises no
|
|
9
|
+
* default at all.
|
|
10
|
+
*
|
|
11
|
+
* Writes are single-process and atomic, but *not* as serialised as the rest
|
|
12
|
+
* of the page: the Settings page disables only the row being saved, so two
|
|
13
|
+
* rows saved back to back can overlap. The write chain below serialises them,
|
|
14
|
+
* so no update is lost to a read-modify-write race. Every read comes from the
|
|
15
|
+
* in-memory snapshot, so the on-disk file only needs to survive a restart: a
|
|
16
|
+
* malformed file reads as empty and is rewritten on the next save, never
|
|
17
|
+
* taking the plugin down with it.
|
|
18
|
+
*/
|
|
19
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
20
|
+
import { dirname } from 'node:path';
|
|
21
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
22
|
+
import { PROVIDER_IDS } from './auth/store.js';
|
|
23
|
+
/** Absolute path of the defaults file. */
|
|
24
|
+
export function modelDefaultsFilePath() {
|
|
25
|
+
return dshHomePath('plugins', 'subscriptions', 'model-defaults.json');
|
|
26
|
+
}
|
|
27
|
+
const EMPTY = Object.freeze({});
|
|
28
|
+
/** In-memory snapshot read by every consumer (adapters, RPC). */
|
|
29
|
+
let current = EMPTY;
|
|
30
|
+
/** One lazy load of the on-disk file (read once per process). */
|
|
31
|
+
let ready;
|
|
32
|
+
/** Last load failure, surfaced to callers that care; defaults stay empty. */
|
|
33
|
+
let loadError;
|
|
34
|
+
/**
|
|
35
|
+
* Serialises every write: the read-modify-write sequence must not interleave,
|
|
36
|
+
* or a fast second save would compute its snapshot from the stale `current`
|
|
37
|
+
* and silently drop the first update (the UI disables only the row being
|
|
38
|
+
* saved, so overlaps are reachable).
|
|
39
|
+
*/
|
|
40
|
+
let writeChain = Promise.resolve();
|
|
41
|
+
/**
|
|
42
|
+
* Validate one persisted provider section: a string→string map, or undefined.
|
|
43
|
+
* Malformed *entries* are skipped, not the whole section: one bad value (a
|
|
44
|
+
* hand edit losing its quotes) must not silently un-configure every model in
|
|
45
|
+
* that provider. What was dropped is reported so the caller can surface it
|
|
46
|
+
* instead of the loss disappearing.
|
|
47
|
+
*/
|
|
48
|
+
function sanitizeProvider(value, dropped) {
|
|
49
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
50
|
+
return undefined;
|
|
51
|
+
const entries = {};
|
|
52
|
+
for (const [model, effort] of Object.entries(value)) {
|
|
53
|
+
if (typeof effort !== 'string' || effort.length === 0) {
|
|
54
|
+
dropped.push(model);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
entries[model] = effort;
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(entries).length === 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
return Object.freeze(entries);
|
|
62
|
+
}
|
|
63
|
+
/** Validate the raw document: only known providers, malformed sections dropped. */
|
|
64
|
+
function sanitizeDefaults(value) {
|
|
65
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
66
|
+
return { defaults: EMPTY, dropped: [] };
|
|
67
|
+
const record = value;
|
|
68
|
+
const result = {};
|
|
69
|
+
const dropped = [];
|
|
70
|
+
for (const provider of PROVIDER_IDS) {
|
|
71
|
+
const section = sanitizeProvider(record[provider], dropped);
|
|
72
|
+
if (section !== undefined)
|
|
73
|
+
result[provider] = section;
|
|
74
|
+
}
|
|
75
|
+
return { defaults: Object.freeze(result), dropped };
|
|
76
|
+
}
|
|
77
|
+
/** Read and validate the on-disk file; a missing file reads as empty. */
|
|
78
|
+
async function loadFile(path) {
|
|
79
|
+
let text;
|
|
80
|
+
try {
|
|
81
|
+
text = await readFile(path, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code === 'ENOENT')
|
|
85
|
+
return EMPTY;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const { defaults, dropped } = sanitizeDefaults(JSON.parse(text));
|
|
90
|
+
if (dropped.length > 0)
|
|
91
|
+
loadError = new Error(`subscriptions model defaults: ${dropped.length} malformed entr${dropped.length === 1 ? 'y' : 'ies'} skipped (${dropped.join(', ')}); fix or delete the file`);
|
|
92
|
+
return defaults;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error(`subscriptions model defaults at ${path} are not valid JSON; fix or delete the file`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Resolve the module state once from disk; failures leave the defaults empty. */
|
|
99
|
+
async function ensureReady() {
|
|
100
|
+
ready ??= loadFile(modelDefaultsFilePath()).then((loaded) => {
|
|
101
|
+
current = loaded;
|
|
102
|
+
// loadFile itself sets loadError for skipped entries; do not clobber it.
|
|
103
|
+
}, (error) => {
|
|
104
|
+
loadError = error;
|
|
105
|
+
current = EMPTY;
|
|
106
|
+
});
|
|
107
|
+
return ready;
|
|
108
|
+
}
|
|
109
|
+
/** Persist a snapshot atomically with owner-only permissions. */
|
|
110
|
+
async function atomicPersist(defaults, path) {
|
|
111
|
+
await mkdir(dirname(path), { recursive: true });
|
|
112
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
113
|
+
try {
|
|
114
|
+
await writeFile(tmp, JSON.stringify(defaults, null, 2), { mode: 0o600 });
|
|
115
|
+
await chmod(tmp, 0o600);
|
|
116
|
+
await rename(tmp, path);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
await rm(tmp, { force: true });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let persistDefaults = atomicPersist;
|
|
124
|
+
/**
|
|
125
|
+
* Clone one provider section, or undefined when nothing is configured for it.
|
|
126
|
+
* The clone is prototype-less: model ids are provider-supplied catalog data
|
|
127
|
+
* used as object keys, and consumers index the section directly (the RPC
|
|
128
|
+
* catalog in index.ts does), so an id like `toString` would otherwise yield an
|
|
129
|
+
* inherited *function* where a string is declared.
|
|
130
|
+
*/
|
|
131
|
+
function sectionOf(defaults, provider) {
|
|
132
|
+
const section = defaults[provider];
|
|
133
|
+
if (section === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
return Object.assign(Object.create(null), section);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Ready the defaults store.
|
|
139
|
+
* @internal Exported for tests; index.ts calls it at apply time so every
|
|
140
|
+
* later synchronous read sees the persisted state.
|
|
141
|
+
*/
|
|
142
|
+
export async function loadModelDefaults() {
|
|
143
|
+
await ensureReady();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The last load failure, or a warning about entries that were skipped while
|
|
147
|
+
* loading; consumers only use it for diagnostics.
|
|
148
|
+
*/
|
|
149
|
+
export function modelDefaultsLoadError() {
|
|
150
|
+
return loadError;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The configured default effort for one model, or undefined when none (the
|
|
154
|
+
* picker then follows the provider's own default).
|
|
155
|
+
* @internal Exported for the adapters' `defaultEffortOf` options.
|
|
156
|
+
*/
|
|
157
|
+
export function defaultEffortOf(provider, model) {
|
|
158
|
+
const section = current[provider];
|
|
159
|
+
if (section === undefined)
|
|
160
|
+
return undefined;
|
|
161
|
+
// Own-property lookup: a model id is provider-supplied catalog data, and a
|
|
162
|
+
// plain index would inherit from Object.prototype for names like
|
|
163
|
+
// `toString`, handing a *function* to mergeReasoning (which then throws and
|
|
164
|
+
// breaks that model's resolution).
|
|
165
|
+
return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : undefined;
|
|
166
|
+
}
|
|
167
|
+
/** A detached snapshot for the RPC surface (render + diffing). */
|
|
168
|
+
export function modelDefaultsSnapshot() {
|
|
169
|
+
const result = {};
|
|
170
|
+
for (const provider of PROVIDER_IDS) {
|
|
171
|
+
const section = sectionOf(current, provider);
|
|
172
|
+
if (section !== undefined)
|
|
173
|
+
result[provider] = section;
|
|
174
|
+
}
|
|
175
|
+
return Object.freeze(result);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Set or clear one model's configured default effort, then persist. The
|
|
179
|
+
* memory snapshot updates only after the atomic write succeeds, so a failed
|
|
180
|
+
* write never leaves the live state ahead of the file.
|
|
181
|
+
* @param provider - the subscription provider route.
|
|
182
|
+
* @param model - the wire model id.
|
|
183
|
+
* @param effort - the effort id, or undefined to clear the override.
|
|
184
|
+
*/
|
|
185
|
+
export function setDefaultEffort(provider, model, effort) {
|
|
186
|
+
// Chained behind every earlier write: the snapshot `current` is read inside
|
|
187
|
+
// the chain, so two overlapping saves cannot lose either update. The caller
|
|
188
|
+
// receives the promise of its own write (a rejection propagates), not the
|
|
189
|
+
// shared chain.
|
|
190
|
+
const run = writeChain.then(async () => {
|
|
191
|
+
await ensureReady();
|
|
192
|
+
const section = { ...sectionOf(current, provider) ?? {} };
|
|
193
|
+
if (effort === undefined) {
|
|
194
|
+
delete section[model];
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
section[model] = effort;
|
|
198
|
+
}
|
|
199
|
+
const next = { ...current };
|
|
200
|
+
if (Object.keys(section).length === 0) {
|
|
201
|
+
delete next[provider];
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
next[provider] = Object.freeze(section);
|
|
205
|
+
}
|
|
206
|
+
const frozen = Object.freeze(next);
|
|
207
|
+
await persistDefaults(frozen, modelDefaultsFilePath());
|
|
208
|
+
current = frozen;
|
|
209
|
+
});
|
|
210
|
+
// Keep the chain alive even when one write fails, or every later save would
|
|
211
|
+
// be stuck behind the rejected promise. The caller has already received the
|
|
212
|
+
// rejection through `run`.
|
|
213
|
+
writeChain = run.catch(() => undefined);
|
|
214
|
+
return run;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Drop the in-memory state and the cached load. Test-only: lets a suite
|
|
218
|
+
* unwind the lazy singleton before the next `loadModelDefaults`.
|
|
219
|
+
* @internal Exported for tests only; not part of the plugin's public surface.
|
|
220
|
+
*/
|
|
221
|
+
export async function resetModelDefaultsForTests() {
|
|
222
|
+
current = EMPTY;
|
|
223
|
+
ready = undefined;
|
|
224
|
+
loadError = undefined;
|
|
225
|
+
writeChain = Promise.resolve();
|
|
226
|
+
persistDefaults = atomicPersist;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Test-only seam: replace the atomic persistence so a failure happens on the
|
|
230
|
+
* real write path. Proves a failed write propagates to the caller and does
|
|
231
|
+
* not wedge the write chain (resetModelDefaultsForTests restores the real
|
|
232
|
+
* implementation).
|
|
233
|
+
* @internal Exported for tests only.
|
|
234
|
+
*/
|
|
235
|
+
export function overridePersistForTests(persist) {
|
|
236
|
+
persistDefaults = persist;
|
|
237
|
+
}
|
|
@@ -12,6 +12,7 @@ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
|
12
12
|
import type { TranslatableMessage } from '../translate/resolved.js';
|
|
13
13
|
import { AccountTokenManager } from './accounts.js';
|
|
14
14
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
15
|
+
import type { RateLimitResetReader, RateLimitWait } from './rate-limit.js';
|
|
15
16
|
export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
16
17
|
export declare const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
17
18
|
export declare const CLAUDE_TOKEN_URL = "https://claude.ai/v1/oauth/token";
|
|
@@ -22,6 +23,20 @@ export declare const CLAUDE_SCOPE = "org:create_api_key user:profile user:infere
|
|
|
22
23
|
export declare const CLAUDE_CALLBACK_PATH = "/callback";
|
|
23
24
|
/** Refresh when the access token has less than this much life left. */
|
|
24
25
|
export declare const CLAUDE_PREEMPT_MS: number;
|
|
26
|
+
/**
|
|
27
|
+
* Reads the reset instant of the Anthropic window that rejected a request.
|
|
28
|
+
*
|
|
29
|
+
* `anthropic-ratelimit-unified-*` is the subscription-plan family — the one
|
|
30
|
+
* Claude Code renders as "resets 3pm" — and is the only header that names the
|
|
31
|
+
* window which actually rejected this request. The per-bucket
|
|
32
|
+
* `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-reset`
|
|
33
|
+
* headers are deliberately not read: they are rollover snapshots attached to
|
|
34
|
+
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
35
|
+
* earliest of them is typically the bucket that still had room — a wait that
|
|
36
|
+
* lands straight back in the closed window. They reach the operator through
|
|
37
|
+
* `rateLimitDiagnostics` instead.
|
|
38
|
+
*/
|
|
39
|
+
export declare const claudeRateLimitReset: RateLimitResetReader;
|
|
25
40
|
/**
|
|
26
41
|
* The subscription endpoint only serves requests presenting as Claude Code,
|
|
27
42
|
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
@@ -78,12 +93,18 @@ export interface ClaudeAdapterOptions {
|
|
|
78
93
|
discovery: boolean;
|
|
79
94
|
fetchFn?: FetchFn;
|
|
80
95
|
onWarn?: (message: string) => void;
|
|
81
|
-
/**
|
|
82
|
-
|
|
96
|
+
/** How long this route may hold a turn open waiting for a rate-limit window; defaults to waiting on, six-hour ceiling. */
|
|
97
|
+
rateLimit?: RateLimitWait;
|
|
83
98
|
/** Resolve the attachment service per request; absent means image requests fail loudly. */
|
|
84
99
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
85
100
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
86
101
|
catalogStore?: CatalogPersistence;
|
|
102
|
+
/**
|
|
103
|
+
* Per-model default reasoning effort override (the Settings page's picker).
|
|
104
|
+
* Returns the user-configured default for one model, or undefined to follow
|
|
105
|
+
* the provider's own default.
|
|
106
|
+
*/
|
|
107
|
+
defaultEffortOf?: (model: string) => string | undefined;
|
|
87
108
|
}
|
|
88
109
|
/**
|
|
89
110
|
* Assemble the Anthropic request body.
|
|
@@ -118,7 +139,7 @@ export declare class ClaudeAdapter extends LlmAdapter {
|
|
|
118
139
|
private discovered;
|
|
119
140
|
private staticModels;
|
|
120
141
|
providerInfo(provider: string): LlmProviderInfo;
|
|
121
|
-
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy
|
|
142
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
|
|
122
143
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
123
144
|
/** The provider's own catalog: union of every account, or one account when named. */
|
|
124
145
|
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
package/lib/providers/claude.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* the Anthropic Messages API with the Claude Code identity headers.
|
|
5
5
|
*/
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
|
-
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId
|
|
7
|
+
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
9
9
|
import { markMessageCache, streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
|
|
10
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
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';
|
|
12
12
|
import { proxiedFetch } from '../http.js';
|
|
13
|
+
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, earliestReset, jsonBody, resetFromFields, resetInstantFromHeader, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
13
14
|
export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
14
15
|
export const CLAUDE_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
|
|
15
16
|
export const CLAUDE_TOKEN_URL = 'https://claude.ai/v1/oauth/token';
|
|
@@ -22,6 +23,30 @@ const CLAUDE_CONTEXT_WINDOW = 200_000;
|
|
|
22
23
|
const CLAUDE_DEFAULT_MAX_TOKENS = 32_000;
|
|
23
24
|
/** Refresh when the access token has less than this much life left. */
|
|
24
25
|
export const CLAUDE_PREEMPT_MS = 5 * 60_000;
|
|
26
|
+
/**
|
|
27
|
+
* Body fields Anthropic uses to name a reset instant, read when the unified
|
|
28
|
+
* headers are absent.
|
|
29
|
+
*/
|
|
30
|
+
const CLAUDE_RESET_FIELDS = ['resets_at', 'resetsAt', 'reset_at', 'retry_after'];
|
|
31
|
+
/**
|
|
32
|
+
* Reads the reset instant of the Anthropic window that rejected a request.
|
|
33
|
+
*
|
|
34
|
+
* `anthropic-ratelimit-unified-*` is the subscription-plan family — the one
|
|
35
|
+
* Claude Code renders as "resets 3pm" — and is the only header that names the
|
|
36
|
+
* window which actually rejected this request. The per-bucket
|
|
37
|
+
* `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-reset`
|
|
38
|
+
* headers are deliberately not read: they are rollover snapshots attached to
|
|
39
|
+
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
40
|
+
* earliest of them is typically the bucket that still had room — a wait that
|
|
41
|
+
* lands straight back in the closed window. They reach the operator through
|
|
42
|
+
* `rateLimitDiagnostics` instead.
|
|
43
|
+
*/
|
|
44
|
+
export const claudeRateLimitReset = (response, body, now) => {
|
|
45
|
+
const unified = earliestReset(resetInstantFromHeader(response, 'anthropic-ratelimit-unified-reset', now), resetInstantFromHeader(response, 'anthropic-ratelimit-unified-fallback-reset', now));
|
|
46
|
+
if (unified !== undefined)
|
|
47
|
+
return unified;
|
|
48
|
+
return resetFromFields(jsonBody(body), CLAUDE_RESET_FIELDS, now);
|
|
49
|
+
};
|
|
25
50
|
/**
|
|
26
51
|
* The subscription endpoint only serves requests presenting as Claude Code,
|
|
27
52
|
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
@@ -324,14 +349,6 @@ export async function fetchClaudeModels(session, fetchFn = proxiedFetch, signal)
|
|
|
324
349
|
}
|
|
325
350
|
return models;
|
|
326
351
|
}
|
|
327
|
-
/**
|
|
328
|
-
* Claude Code's own SDK retry shape: exponential backoff starting at 1s,
|
|
329
|
-
* doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
|
|
330
|
-
* count of retries after the first attempt (Claude Code defaults to 10).
|
|
331
|
-
*/
|
|
332
|
-
const CLAUDE_RETRY_INITIAL_DELAY_MS = 1_000;
|
|
333
|
-
const CLAUDE_RETRY_MAX_DELAY_MS = 60_000;
|
|
334
|
-
const CLAUDE_RETRY_JITTER_RATIO = 0.2;
|
|
335
352
|
/** The Claude 4.5 family accepts image input. */
|
|
336
353
|
const CLAUDE_MODALITIES = ['text', 'image'];
|
|
337
354
|
/**
|
|
@@ -433,17 +450,7 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
433
450
|
return { id: provider, name: 'Claude (Subscription)' };
|
|
434
451
|
}
|
|
435
452
|
providerRetryPolicy(provider) {
|
|
436
|
-
|
|
437
|
-
return undefined;
|
|
438
|
-
return resolveRetryPolicy({
|
|
439
|
-
mode: 'normal',
|
|
440
|
-
maxRetries: this.options.maxRetries,
|
|
441
|
-
backoff: {
|
|
442
|
-
initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
|
|
443
|
-
maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
|
|
444
|
-
jitterRatio: CLAUDE_RETRY_JITTER_RATIO,
|
|
445
|
-
},
|
|
446
|
-
}, `claude: provider "${provider}" retryPolicy`);
|
|
453
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `claude: provider "${provider}" retryPolicy`);
|
|
447
454
|
}
|
|
448
455
|
async listModels(provider) {
|
|
449
456
|
const own = await this.listOwnModels(provider);
|
|
@@ -498,7 +505,7 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
498
505
|
async resolveOwnModel(provider, model) {
|
|
499
506
|
const disc = await this.discovered(model);
|
|
500
507
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
501
|
-
const reasoning = disc?.reasoning;
|
|
508
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), disc?.reasoning);
|
|
502
509
|
return {
|
|
503
510
|
provider,
|
|
504
511
|
id: model,
|
|
@@ -532,8 +539,12 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
532
539
|
session = await this.options.tokens.session(account, true);
|
|
533
540
|
response = await this.request(options, session, watchdog.signal);
|
|
534
541
|
}
|
|
535
|
-
if (!response.ok)
|
|
536
|
-
throw await httpLlmError(response, 'claude API'
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
throw await httpLlmError(response, 'claude API', {
|
|
544
|
+
rateLimitReset: claudeRateLimitReset,
|
|
545
|
+
...this.options.onWarn === undefined ? {} : { onWarn: this.options.onWarn },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
537
548
|
if (response.body === null) {
|
|
538
549
|
throw new LlmError('claude API returned no response body', EMPTY_RESPONSE_CODE);
|
|
539
550
|
}
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -12,12 +12,24 @@ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
|
12
12
|
import type { ResponsesRequestInput } from '../translate/responses.js';
|
|
13
13
|
import { AccountTokenManager } from './accounts.js';
|
|
14
14
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
15
|
+
import type { RateLimitResetReader, RateLimitWait } from './rate-limit.js';
|
|
15
16
|
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
16
17
|
export declare const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
17
18
|
export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
18
19
|
export declare const CODEX_API_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
19
20
|
/** Refresh when the access token has less than this much life left. */
|
|
20
21
|
export declare const CODEX_PREEMPT_MS: number;
|
|
22
|
+
/**
|
|
23
|
+
* Reads the reset instant of the Codex window that rejected a request.
|
|
24
|
+
*
|
|
25
|
+
* Body only. The `x-codex-{primary,secondary}-reset-after-seconds` headers are
|
|
26
|
+
* rollover snapshots the backend attaches to every response, one per window,
|
|
27
|
+
* so they say nothing about which window refused: a burst 429 that would clear
|
|
28
|
+
* in seconds still carries a primary rollover hours out, and reading it would
|
|
29
|
+
* park the turn for those hours. They reach the operator through
|
|
30
|
+
* `rateLimitDiagnostics` instead.
|
|
31
|
+
*/
|
|
32
|
+
export declare const codexRateLimitReset: RateLimitResetReader;
|
|
21
33
|
/**
|
|
22
34
|
* Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
|
|
23
35
|
* value for priority processing, mirroring codex-rs
|
|
@@ -114,6 +126,14 @@ export interface CodexAdapterOptions {
|
|
|
114
126
|
catalogStore?: CatalogPersistence;
|
|
115
127
|
/** Per-account catalog bound for the picker union (defaults to {@link DISCOVERY_TIMEOUT_MS}). */
|
|
116
128
|
discoveryTimeoutMs?: number;
|
|
129
|
+
/** How long this route may hold a turn open waiting for a rate-limit window; defaults to waiting on, six-hour ceiling. */
|
|
130
|
+
rateLimit?: RateLimitWait;
|
|
131
|
+
/**
|
|
132
|
+
* Per-model default reasoning effort override (the Settings page's picker).
|
|
133
|
+
* Returns the user-configured default for one model, or undefined to follow
|
|
134
|
+
* the provider's own default.
|
|
135
|
+
*/
|
|
136
|
+
defaultEffortOf?: (model: string) => string | undefined;
|
|
117
137
|
/**
|
|
118
138
|
* Per-request speed lookup (the composer Speed toggle's host half). Returns
|
|
119
139
|
* whether this session's current choice sends the model on the fast tier;
|
|
@@ -144,6 +164,7 @@ export declare class CodexAdapter extends LlmAdapter {
|
|
|
144
164
|
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
145
165
|
private catalogFor;
|
|
146
166
|
providerInfo(provider: string): LlmProviderInfo;
|
|
167
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
|
|
147
168
|
private staticModels;
|
|
148
169
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
149
170
|
/** The provider's own catalog: union of every account, or one account when named. */
|
package/lib/providers/codex.js
CHANGED
|
@@ -8,9 +8,10 @@ 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 { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
|
+
import { effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
12
12
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
13
13
|
import { proxiedFetch } from '../http.js';
|
|
14
|
+
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
14
15
|
export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
15
16
|
export const CODEX_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
|
|
16
17
|
export const CODEX_TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
@@ -21,6 +22,23 @@ const CODEX_CONTEXT_WINDOW = 400_000;
|
|
|
21
22
|
const CODEX_DEFAULT_MAX_TOKENS = 128_000;
|
|
22
23
|
/** Refresh when the access token has less than this much life left. */
|
|
23
24
|
export const CODEX_PREEMPT_MS = 5 * 60_000;
|
|
25
|
+
/**
|
|
26
|
+
* Body fields the backend uses to name a reset. A window-exhaustion rejection
|
|
27
|
+
* carries `usage_limit_reached` with the seconds left on the window — the case
|
|
28
|
+
* that used to classify as a terminal quota and never be retried at all.
|
|
29
|
+
*/
|
|
30
|
+
const CODEX_RESET_FIELDS = ['resets_in_seconds', 'reset_after_seconds', 'resets_at', 'reset_at'];
|
|
31
|
+
/**
|
|
32
|
+
* Reads the reset instant of the Codex window that rejected a request.
|
|
33
|
+
*
|
|
34
|
+
* Body only. The `x-codex-{primary,secondary}-reset-after-seconds` headers are
|
|
35
|
+
* rollover snapshots the backend attaches to every response, one per window,
|
|
36
|
+
* so they say nothing about which window refused: a burst 429 that would clear
|
|
37
|
+
* in seconds still carries a primary rollover hours out, and reading it would
|
|
38
|
+
* park the turn for those hours. They reach the operator through
|
|
39
|
+
* `rateLimitDiagnostics` instead.
|
|
40
|
+
*/
|
|
41
|
+
export const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
24
42
|
/** Default instruction when the request carries no system prompt. */
|
|
25
43
|
const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, a coding agent based on GPT-5. '
|
|
26
44
|
+ 'Help the user with their software engineering tasks.';
|
|
@@ -292,10 +310,6 @@ export const CODEX_MODELS_URL = 'https://chatgpt.com/backend-api/codex/models';
|
|
|
292
310
|
* the range of current codex CLI releases.
|
|
293
311
|
*/
|
|
294
312
|
export const CODEX_CLIENT_VERSION = '0.147.0';
|
|
295
|
-
/** Display name for a wire reasoning-effort value. */
|
|
296
|
-
function effortName(effort) {
|
|
297
|
-
return effort === 'xhigh' ? 'Extra High' : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
298
|
-
}
|
|
299
313
|
/**
|
|
300
314
|
* Whether a catalog entry advertises the fast tier. Mirrors codex-rs
|
|
301
315
|
* `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
|
|
@@ -341,7 +355,7 @@ export async function fetchCodexModels(session, fetchFn = proxiedFetch, signal)
|
|
|
341
355
|
.filter(level => typeof level.effort === 'string' && level.effort.length > 0)
|
|
342
356
|
.map(level => ({
|
|
343
357
|
id: ReasoningEffortId(level.effort),
|
|
344
|
-
name:
|
|
358
|
+
name: effortDisplayName(level.effort),
|
|
345
359
|
...level.description === undefined ? {} : { description: level.description },
|
|
346
360
|
}));
|
|
347
361
|
const defaultEffort = typeof entry.default_reasoning_level === 'string'
|
|
@@ -499,6 +513,9 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
499
513
|
providerInfo(provider) {
|
|
500
514
|
return { id: provider, name: 'ChatGPT (Codex)' };
|
|
501
515
|
}
|
|
516
|
+
providerRetryPolicy(provider) {
|
|
517
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `codex: provider "${provider}" retryPolicy`);
|
|
518
|
+
}
|
|
502
519
|
staticModels(provider) {
|
|
503
520
|
return this.options.models.map(model => ({
|
|
504
521
|
provider,
|
|
@@ -618,9 +635,15 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
618
635
|
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
619
636
|
async resolveOwnModel(provider, model) {
|
|
620
637
|
// Discovered metadata (when discovery is on) wins over the static entry;
|
|
621
|
-
// the static entry wins over the built-in defaults.
|
|
638
|
+
// the static entry wins over the built-in defaults. A configured default
|
|
639
|
+
// effort merges over both.
|
|
622
640
|
const discovered = await this.discovered(model);
|
|
623
641
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
642
|
+
// `extendable` only while falling back to the built-in list: that one is
|
|
643
|
+
// known to trail the backend, so a configured level it omits still has to
|
|
644
|
+
// be selectable. A discovered catalog is the truth about what the model
|
|
645
|
+
// accepts, and a stale override must not be forced onto every request.
|
|
646
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning ?? { efforts: CODEX_EFFORTS, defaultEffort: CODEX_DEFAULT_EFFORT }, { extendable: discovered?.reasoning === undefined });
|
|
624
647
|
return {
|
|
625
648
|
provider,
|
|
626
649
|
id: model,
|
|
@@ -629,7 +652,7 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
629
652
|
inputModalities: configured?.inputModalities ?? CODEX_MODALITIES,
|
|
630
653
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? CODEX_CONTEXT_WINDOW },
|
|
631
654
|
defaultMaxTokens: configured?.maxTokens ?? CODEX_DEFAULT_MAX_TOKENS,
|
|
632
|
-
reasoning
|
|
655
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
633
656
|
};
|
|
634
657
|
}
|
|
635
658
|
async *stream(options) {
|
|
@@ -654,8 +677,12 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
654
677
|
session = await this.options.tokens.session(account, true);
|
|
655
678
|
response = await this.request(options, session, watchdog.signal);
|
|
656
679
|
}
|
|
657
|
-
if (!response.ok)
|
|
658
|
-
throw await httpLlmError(response, 'codex API'
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
throw await httpLlmError(response, 'codex API', {
|
|
682
|
+
rateLimitReset: codexRateLimitReset,
|
|
683
|
+
...this.options.onWarn === undefined ? {} : { onWarn: this.options.onWarn },
|
|
684
|
+
});
|
|
685
|
+
}
|
|
659
686
|
if (response.body === null) {
|
|
660
687
|
throw new LlmError('codex API returned no response body', EMPTY_RESPONSE_CODE);
|
|
661
688
|
}
|