dsh-plugin-subscriptions 0.4.1 → 0.5.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 +9 -5
- package/README.zh.md +9 -5
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +167 -0
- package/lib/auth/pkce.js +1 -1
- package/lib/auth/rpc.d.ts +18 -1
- package/lib/auth/rpc.js +23 -3
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/ImageGenerateToolview.js +2 -2
- package/lib/client/SpeedSelect.d.ts +48 -0
- package/lib/client/SpeedSelect.js +173 -0
- package/lib/client/SubscriptionsSection.d.ts +9 -0
- package/lib/client/SubscriptionsSection.js +8 -2
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +42 -0
- package/lib/client/locales.d.ts +14 -0
- package/lib/client/locales.js +14 -0
- package/lib/client.js +590 -74
- package/lib/client.js.map +1 -1
- package/lib/index.js +504 -90
- package/lib/providers/catalog-store.js +8 -0
- package/lib/providers/claude.d.ts +35 -3
- package/lib/providers/claude.js +193 -29
- package/lib/providers/codex.d.ts +27 -0
- package/lib/providers/codex.js +66 -20
- package/lib/providers/common.d.ts +4 -0
- package/lib/tools/image-generate.js +3 -10
- package/package.json +3 -1
|
@@ -71,6 +71,12 @@ function sanitizeModel(value) {
|
|
|
71
71
|
const reasoning = raw.reasoning === undefined ? undefined : sanitizeReasoning(raw.reasoning);
|
|
72
72
|
if (raw.reasoning !== undefined && reasoning === undefined)
|
|
73
73
|
return undefined;
|
|
74
|
+
const thinkingType = raw.thinkingType;
|
|
75
|
+
if (thinkingType !== undefined && thinkingType !== 'enabled' && thinkingType !== 'adaptive')
|
|
76
|
+
return undefined;
|
|
77
|
+
const fastTier = raw.fastTier;
|
|
78
|
+
if (fastTier !== undefined && typeof fastTier !== 'boolean')
|
|
79
|
+
return undefined;
|
|
74
80
|
return {
|
|
75
81
|
id: raw.id,
|
|
76
82
|
name: raw.name,
|
|
@@ -78,6 +84,8 @@ function sanitizeModel(value) {
|
|
|
78
84
|
...raw.contextWindow === undefined ? {} : { contextWindow: raw.contextWindow },
|
|
79
85
|
...raw.priority === undefined ? {} : { priority: raw.priority },
|
|
80
86
|
...reasoning === undefined ? {} : { reasoning },
|
|
87
|
+
...thinkingType === undefined ? {} : { thinkingType: thinkingType },
|
|
88
|
+
...fastTier === undefined ? {} : { fastTier },
|
|
81
89
|
};
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
@@ -9,14 +9,23 @@ import type { FlowSpec } from '../auth/oauth-flow.js';
|
|
|
9
9
|
import type { ClaudeSession } from '../auth/store.js';
|
|
10
10
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
11
|
import { TokenManager } from './common.js';
|
|
12
|
-
import type { FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
12
|
+
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
13
13
|
export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
14
|
-
export declare const CLAUDE_AUTHORIZE_URL = "https://claude.
|
|
15
|
-
export declare const CLAUDE_TOKEN_URL = "https://
|
|
14
|
+
export declare const CLAUDE_AUTHORIZE_URL = "https://claude.com/cai/oauth/authorize";
|
|
15
|
+
export declare const CLAUDE_TOKEN_URL = "https://claude.ai/v1/oauth/token";
|
|
16
16
|
export declare const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
|
|
17
17
|
export declare const CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
18
|
+
export declare const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?beta=true";
|
|
18
19
|
/** Refresh when the access token has less than this much life left. */
|
|
19
20
|
export declare const CLAUDE_PREEMPT_MS: number;
|
|
21
|
+
/**
|
|
22
|
+
* The subscription endpoint only serves requests presenting as Claude Code,
|
|
23
|
+
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
24
|
+
* cannot be sent here (one user-agent slot, and the CLI's wins).
|
|
25
|
+
*/
|
|
26
|
+
export declare const CLAUDE_CLI_FALLBACK_VERSION = "2.1.234";
|
|
27
|
+
export declare function detectClaudeVersion(): string;
|
|
28
|
+
export declare const CLAUDE_BETA_FALLBACK: string;
|
|
20
29
|
/** Static claude flow facts for the OAuth flow engine. */
|
|
21
30
|
export declare const claudeFlow: FlowSpec;
|
|
22
31
|
/**
|
|
@@ -52,21 +61,44 @@ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usa
|
|
|
52
61
|
* @returns the mapped usage snapshot.
|
|
53
62
|
*/
|
|
54
63
|
export declare function fetchClaudeUsage(session: ClaudeSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
64
|
+
/** Fetch the live model catalog from the subscription endpoint. */
|
|
65
|
+
export declare function fetchClaudeModels(session: ClaudeSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
|
|
55
66
|
/** Constructor dependencies for {@link ClaudeAdapter}. */
|
|
56
67
|
export interface ClaudeAdapterOptions {
|
|
57
68
|
models: readonly ModelEntry[];
|
|
58
69
|
streamIdleTimeoutMs: number;
|
|
59
70
|
tokens: TokenManager<ClaudeSession>;
|
|
71
|
+
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
72
|
+
discovery: boolean;
|
|
73
|
+
fetchFn?: FetchFn;
|
|
74
|
+
onWarn?: (message: string) => void;
|
|
75
|
+
/** Max retries on a retryable failure before giving up; matches Claude Code's own client-side retry count. Defaults to the dsh-llm default (2) when unset. */
|
|
76
|
+
maxRetries?: number;
|
|
60
77
|
/** Resolve the attachment service per request; absent means image requests fail loudly. */
|
|
61
78
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
79
|
+
/** Durable catalog store seeding capability metadata across restarts. */
|
|
80
|
+
catalogStore?: CatalogPersistence;
|
|
62
81
|
}
|
|
63
82
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
64
83
|
export declare class ClaudeAdapter extends LlmAdapter {
|
|
65
84
|
private readonly options;
|
|
85
|
+
private readonly catalog;
|
|
66
86
|
constructor(options: ClaudeAdapterOptions);
|
|
87
|
+
private fetchCatalog;
|
|
88
|
+
private discovered;
|
|
89
|
+
private staticModels;
|
|
67
90
|
providerInfo(provider: string): LlmProviderInfo;
|
|
91
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy | undefined;
|
|
68
92
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
69
93
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
70
94
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
95
|
+
/**
|
|
96
|
+
* `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
|
|
97
|
+
* models default to `display: 'omitted'`, which returns thinking blocks with
|
|
98
|
+
* an empty `thinking` field — without this override the "Think" panel would
|
|
99
|
+
* always render empty even though real reasoning (and billed thinking_tokens)
|
|
100
|
+
* ran.
|
|
101
|
+
*/
|
|
102
|
+
private thinkingParam;
|
|
71
103
|
private request;
|
|
72
104
|
}
|
package/lib/providers/claude.js
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* platform.claude.com with the Claude Code client id, and streaming against
|
|
4
4
|
* the Anthropic Messages API with the Claude Code identity headers.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
7
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
8
9
|
import { streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
|
|
9
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
10
11
|
export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
11
|
-
export const CLAUDE_AUTHORIZE_URL = 'https://claude.
|
|
12
|
-
export const CLAUDE_TOKEN_URL = 'https://
|
|
12
|
+
export const CLAUDE_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
|
|
13
|
+
export const CLAUDE_TOKEN_URL = 'https://claude.ai/v1/oauth/token';
|
|
13
14
|
export const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages?beta=true';
|
|
14
15
|
export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
16
|
+
export const CLAUDE_MODELS_URL = 'https://api.anthropic.com/v1/models?beta=true';
|
|
15
17
|
const CLAUDE_SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
|
|
16
18
|
const CLAUDE_CALLBACK_PATH = '/callback';
|
|
17
19
|
const CLAUDE_CONTEXT_WINDOW = 200_000;
|
|
@@ -23,19 +25,49 @@ export const CLAUDE_PREEMPT_MS = 5 * 60_000;
|
|
|
23
25
|
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
24
26
|
* cannot be sent here (one user-agent slot, and the CLI's wins).
|
|
25
27
|
*/
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
+
export const CLAUDE_CLI_FALLBACK_VERSION = '2.1.234';
|
|
29
|
+
export function detectClaudeVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = execFileSync('claude', ['--version'], { timeout: 3000, encoding: 'utf8' });
|
|
32
|
+
const match = raw.match(/^(\d+\.\d+\.\d+)/);
|
|
33
|
+
if (match)
|
|
34
|
+
return match[1];
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
return CLAUDE_CLI_FALLBACK_VERSION;
|
|
38
|
+
}
|
|
39
|
+
// Lazy + memoized: detectClaudeVersion() shells out to `claude --version`,
|
|
40
|
+
// so this must not run at module-evaluation time (it would fire for every
|
|
41
|
+
// consumer of this module regardless of whether Claude is a configured
|
|
42
|
+
// provider). Computed on first use of getClaudeCliUserAgent() instead.
|
|
43
|
+
let claudeCliUserAgent;
|
|
44
|
+
function getClaudeCliUserAgent() {
|
|
45
|
+
if (claudeCliUserAgent === undefined) {
|
|
46
|
+
claudeCliUserAgent = `claude-cli/${detectClaudeVersion()} (external, cli)`;
|
|
47
|
+
}
|
|
48
|
+
return claudeCliUserAgent;
|
|
49
|
+
}
|
|
50
|
+
export const CLAUDE_BETA_FALLBACK = [
|
|
51
|
+
'claude-code-20250219',
|
|
52
|
+
'oauth-2025-04-20',
|
|
53
|
+
'interleaved-thinking-2025-05-14',
|
|
54
|
+
'context-management-2025-06-27',
|
|
55
|
+
'effort-2025-11-24',
|
|
56
|
+
'compact-2026-01-12',
|
|
57
|
+
'files-api-2025-04-14',
|
|
58
|
+
].join(',');
|
|
59
|
+
const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
|
|
28
60
|
/** Static claude flow facts for the OAuth flow engine. */
|
|
29
61
|
export const claudeFlow = {
|
|
30
62
|
callbackPath: CLAUDE_CALLBACK_PATH,
|
|
31
63
|
// The redirect URI embeds the port, so it must be an ephemeral one.
|
|
32
|
-
listen: { host: '
|
|
64
|
+
listen: { host: '127.0.0.1', ports: [0] },
|
|
33
65
|
buildAuthorizeUrl({ redirectUri, state, pkce }) {
|
|
34
66
|
const params = new URLSearchParams({
|
|
35
67
|
code: 'true',
|
|
36
68
|
client_id: CLAUDE_CLIENT_ID,
|
|
37
69
|
response_type: 'code',
|
|
38
|
-
redirect_uri:
|
|
70
|
+
redirect_uri: 'https://platform.claude.com/oauth/code/callback',
|
|
39
71
|
scope: CLAUDE_SCOPE,
|
|
40
72
|
code_challenge: pkce.challenge,
|
|
41
73
|
code_challenge_method: 'S256',
|
|
@@ -212,7 +244,7 @@ export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
|
|
|
212
244
|
'anthropic-beta': 'oauth-2025-04-20',
|
|
213
245
|
// Unrecognized clients are aggressively rate-limited on this endpoint,
|
|
214
246
|
// so it presents as the CLI like every other subscription request.
|
|
215
|
-
'user-agent':
|
|
247
|
+
'user-agent': getClaudeCliUserAgent(),
|
|
216
248
|
'accept': 'application/json',
|
|
217
249
|
},
|
|
218
250
|
...signal === undefined ? {} : { signal },
|
|
@@ -236,24 +268,88 @@ export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
|
|
|
236
268
|
}
|
|
237
269
|
return { supported: true, windows };
|
|
238
270
|
}
|
|
271
|
+
function claudeThinkingType(capabilities) {
|
|
272
|
+
const types = capabilities?.thinking?.types;
|
|
273
|
+
if (types?.enabled?.supported === true)
|
|
274
|
+
return 'enabled';
|
|
275
|
+
if (types?.adaptive?.supported === true)
|
|
276
|
+
return 'adaptive';
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
/** Effort levels in display order; a model exposes only the ones it advertises as supported. */
|
|
280
|
+
const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
281
|
+
function claudeReasoning(capabilities) {
|
|
282
|
+
const effort = capabilities?.effort;
|
|
283
|
+
if (effort?.supported !== true)
|
|
284
|
+
return undefined;
|
|
285
|
+
const efforts = CLAUDE_EFFORT_LEVELS
|
|
286
|
+
.filter(level => effort[level]?.supported === true)
|
|
287
|
+
.map(level => ({ id: ReasoningEffortId(level), name: level[0].toUpperCase() + level.slice(1) }));
|
|
288
|
+
return efforts.length > 0 ? { efforts } : undefined;
|
|
289
|
+
}
|
|
290
|
+
/** Fetch the live model catalog from the subscription endpoint. */
|
|
291
|
+
export async function fetchClaudeModels(session, fetchFn = fetch) {
|
|
292
|
+
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
293
|
+
headers: {
|
|
294
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
295
|
+
'anthropic-version': '2023-06-01',
|
|
296
|
+
'user-agent': getClaudeCliUserAgent(),
|
|
297
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
298
|
+
'accept': 'application/json',
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
if (!response.ok)
|
|
302
|
+
throw await httpLlmError(response, 'claude models API');
|
|
303
|
+
const payload = await response.json();
|
|
304
|
+
if (!Array.isArray(payload.data)) {
|
|
305
|
+
throw new Error('claude models API returned an invalid catalog');
|
|
306
|
+
}
|
|
307
|
+
const models = payload.data
|
|
308
|
+
.filter((m) => typeof m.id === 'string')
|
|
309
|
+
.map((m) => {
|
|
310
|
+
const thinkingType = claudeThinkingType(m.capabilities);
|
|
311
|
+
const reasoning = claudeReasoning(m.capabilities);
|
|
312
|
+
return {
|
|
313
|
+
id: m.id,
|
|
314
|
+
name: m.display_name ?? m.id,
|
|
315
|
+
...thinkingType === undefined ? {} : { thinkingType },
|
|
316
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
if (models.length === 0) {
|
|
320
|
+
throw new Error('claude models API returned an empty catalog');
|
|
321
|
+
}
|
|
322
|
+
return models;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Claude Code's own SDK retry shape: exponential backoff starting at 1s,
|
|
326
|
+
* doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
|
|
327
|
+
* count of retries after the first attempt (Claude Code defaults to 10).
|
|
328
|
+
*/
|
|
329
|
+
const CLAUDE_RETRY_INITIAL_DELAY_MS = 1_000;
|
|
330
|
+
const CLAUDE_RETRY_MAX_DELAY_MS = 60_000;
|
|
331
|
+
const CLAUDE_RETRY_JITTER_RATIO = 0.2;
|
|
239
332
|
/** The Claude 4.5 family accepts image input. */
|
|
240
333
|
const CLAUDE_MODALITIES = ['text', 'image'];
|
|
241
334
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
242
335
|
export class ClaudeAdapter extends LlmAdapter {
|
|
243
336
|
options;
|
|
337
|
+
catalog;
|
|
244
338
|
constructor(options) {
|
|
245
339
|
super();
|
|
246
340
|
this.options = options;
|
|
341
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
247
342
|
}
|
|
248
|
-
|
|
249
|
-
return
|
|
343
|
+
async fetchCatalog() {
|
|
344
|
+
return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
250
345
|
}
|
|
251
|
-
async
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
346
|
+
async discovered(model) {
|
|
347
|
+
if (!this.options.discovery)
|
|
348
|
+
return undefined;
|
|
349
|
+
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
350
|
+
return models?.find(entry => entry.id === model);
|
|
351
|
+
}
|
|
352
|
+
staticModels(provider) {
|
|
257
353
|
return this.options.models.map(model => ({
|
|
258
354
|
provider,
|
|
259
355
|
id: model.id,
|
|
@@ -261,18 +357,61 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
261
357
|
inputModalities: model.inputModalities ?? CLAUDE_MODALITIES,
|
|
262
358
|
}));
|
|
263
359
|
}
|
|
264
|
-
|
|
360
|
+
providerInfo(provider) {
|
|
361
|
+
return { id: provider, name: 'Claude (Subscription)' };
|
|
362
|
+
}
|
|
363
|
+
providerRetryPolicy(provider) {
|
|
364
|
+
if (this.options.maxRetries === undefined)
|
|
365
|
+
return undefined;
|
|
366
|
+
return resolveRetryPolicy({
|
|
367
|
+
mode: 'normal',
|
|
368
|
+
maxRetries: this.options.maxRetries,
|
|
369
|
+
backoff: {
|
|
370
|
+
initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
|
|
371
|
+
maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
|
|
372
|
+
jitterRatio: CLAUDE_RETRY_JITTER_RATIO,
|
|
373
|
+
},
|
|
374
|
+
}, `claude: provider "${provider}" retryPolicy`);
|
|
375
|
+
}
|
|
376
|
+
async listModels(provider) {
|
|
377
|
+
if (await this.options.tokens.peek() === undefined)
|
|
378
|
+
return [];
|
|
379
|
+
if (!this.options.discovery)
|
|
380
|
+
return this.staticModels(provider);
|
|
381
|
+
try {
|
|
382
|
+
const models = await this.catalog.get(() => this.fetchCatalog());
|
|
383
|
+
return models.map(model => ({
|
|
384
|
+
provider,
|
|
385
|
+
id: model.id,
|
|
386
|
+
name: model.name,
|
|
387
|
+
inputModalities: CLAUDE_MODALITIES,
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (error instanceof LlmError
|
|
392
|
+
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
|
|
393
|
+
return [];
|
|
394
|
+
if (error instanceof LlmError && error.code === 'AUTH')
|
|
395
|
+
this.catalog.invalidate();
|
|
396
|
+
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
397
|
+
return this.staticModels(provider);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async resolveModel(provider, model) {
|
|
401
|
+
const disc = await this.discovered(model);
|
|
265
402
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
266
|
-
|
|
403
|
+
const reasoning = disc?.reasoning;
|
|
404
|
+
return {
|
|
267
405
|
provider,
|
|
268
406
|
id: model,
|
|
269
|
-
name: configured?.name ?? model,
|
|
407
|
+
name: disc?.name ?? configured?.name ?? model,
|
|
270
408
|
inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
|
|
271
|
-
context: {
|
|
409
|
+
context: {
|
|
410
|
+
contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW,
|
|
411
|
+
},
|
|
272
412
|
defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
413
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
414
|
+
};
|
|
276
415
|
}
|
|
277
416
|
async *stream(options) {
|
|
278
417
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -280,7 +419,6 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
280
419
|
let session = await this.options.tokens.session();
|
|
281
420
|
let response = await this.request(options, session, watchdog.signal);
|
|
282
421
|
if (response.status === 401) {
|
|
283
|
-
// One forced refresh + retry on an unexpired-but-rejected token.
|
|
284
422
|
session = await this.options.tokens.session(true);
|
|
285
423
|
response = await this.request(options, session, watchdog.signal);
|
|
286
424
|
}
|
|
@@ -298,18 +436,44 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
298
436
|
watchdog.stop();
|
|
299
437
|
}
|
|
300
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
|
|
441
|
+
* models default to `display: 'omitted'`, which returns thinking blocks with
|
|
442
|
+
* an empty `thinking` field — without this override the "Think" panel would
|
|
443
|
+
* always render empty even though real reasoning (and billed thinking_tokens)
|
|
444
|
+
* ran.
|
|
445
|
+
*/
|
|
446
|
+
thinkingParam(thinkingType, maxTokens) {
|
|
447
|
+
if (thinkingType === 'adaptive')
|
|
448
|
+
return { type: 'adaptive', display: 'summarized' };
|
|
449
|
+
if (thinkingType === 'enabled') {
|
|
450
|
+
const budget = Math.min(Math.max(1_024, Math.floor(maxTokens * 0.5)), maxTokens - 100);
|
|
451
|
+
if (budget < 1_024)
|
|
452
|
+
return undefined;
|
|
453
|
+
return { type: 'enabled', budget_tokens: budget, display: 'summarized' };
|
|
454
|
+
}
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
301
457
|
async request(options, session, signal) {
|
|
302
458
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
459
|
+
const maxTokens = options.maxTokens
|
|
460
|
+
?? this.options.models.find(entry => entry.id === options.model)?.maxTokens
|
|
461
|
+
?? CLAUDE_DEFAULT_MAX_TOKENS;
|
|
462
|
+
const disc = await this.discovered(options.model);
|
|
463
|
+
const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
|
|
464
|
+
const effort = options.reasoningEffort !== undefined && disc?.reasoning !== undefined
|
|
465
|
+
? { output_config: { effort: String(options.reasoningEffort) } }
|
|
466
|
+
: {};
|
|
303
467
|
const body = {
|
|
304
468
|
model: options.model,
|
|
305
|
-
max_tokens:
|
|
306
|
-
?? this.options.models.find(entry => entry.id === options.model)?.maxTokens
|
|
307
|
-
?? CLAUDE_DEFAULT_MAX_TOKENS,
|
|
469
|
+
max_tokens: maxTokens,
|
|
308
470
|
system: toAnthropicSystem(options.system, messages),
|
|
309
471
|
messages: toAnthropicMessages(messages),
|
|
310
472
|
...options.tools !== undefined && options.tools.length > 0
|
|
311
473
|
? { tools: toAnthropicTools(options.tools) }
|
|
312
474
|
: {},
|
|
475
|
+
...thinking === undefined ? {} : { thinking },
|
|
476
|
+
...effort,
|
|
313
477
|
stream: true,
|
|
314
478
|
...options.sessionId !== undefined ? { metadata: { user_id: String(options.sessionId) } } : {},
|
|
315
479
|
};
|
|
@@ -319,7 +483,7 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
319
483
|
'authorization': `Bearer ${session.accessToken}`,
|
|
320
484
|
'anthropic-version': '2023-06-01',
|
|
321
485
|
'anthropic-beta': CLAUDE_BETA_FLAGS,
|
|
322
|
-
'user-agent':
|
|
486
|
+
'user-agent': getClaudeCliUserAgent(),
|
|
323
487
|
'x-app': 'cli',
|
|
324
488
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
325
489
|
'accept': 'text/event-stream',
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelIn
|
|
|
8
8
|
import type { FlowSpec } from '../auth/oauth-flow.js';
|
|
9
9
|
import type { CodexSession } from '../auth/store.js';
|
|
10
10
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
|
+
import type { ResponsesRequestInput } from '../translate/responses.js';
|
|
11
12
|
import { TokenManager } from './common.js';
|
|
12
13
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
13
14
|
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
@@ -16,6 +17,15 @@ export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
|
16
17
|
export declare const CODEX_API_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
17
18
|
/** Refresh when the access token has less than this much life left. */
|
|
18
19
|
export declare const CODEX_PREEMPT_MS: number;
|
|
20
|
+
/**
|
|
21
|
+
* Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
|
|
22
|
+
* value for priority processing, mirroring codex-rs
|
|
23
|
+
* `ServiceTier::Fast.request_value()`. The legacy catalog spelling is the
|
|
24
|
+
* `additional_speed_tiers` entry "fast".
|
|
25
|
+
*/
|
|
26
|
+
export declare const CODEX_FAST_SERVICE_TIER = "priority";
|
|
27
|
+
/** One session's speed choice: standard routing or the fast (priority) tier. */
|
|
28
|
+
export type CodexSpeedTier = 'standard' | 'fast';
|
|
19
29
|
/** Static codex flow facts for the OAuth flow engine. */
|
|
20
30
|
export declare const codexFlow: FlowSpec;
|
|
21
31
|
/** User identity claims decoded from a codex id token. */
|
|
@@ -98,7 +108,20 @@ export interface CodexAdapterOptions {
|
|
|
98
108
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
99
109
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
100
110
|
catalogStore?: CatalogPersistence;
|
|
111
|
+
/**
|
|
112
|
+
* Per-request speed lookup (the composer Speed toggle's host half). Returns
|
|
113
|
+
* whether this session's current choice sends the model on the fast tier;
|
|
114
|
+
* absent means every request stays on standard routing.
|
|
115
|
+
*/
|
|
116
|
+
speedFor?: (sessionId: string | undefined, model: string) => Promise<boolean> | boolean;
|
|
101
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The Responses request body for one generation. A fast-tier request (the
|
|
120
|
+
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
121
|
+
* `service_tier: priority`; the tier field is omitted entirely otherwise,
|
|
122
|
+
* matching the CLI (it never sends an explicit standard tier).
|
|
123
|
+
*/
|
|
124
|
+
export declare function codexRequestBody(options: GenerateOptions, resolved: ResponsesRequestInput, fast: boolean): Record<string, unknown>;
|
|
102
125
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
103
126
|
export declare class CodexAdapter extends LlmAdapter {
|
|
104
127
|
private readonly options;
|
|
@@ -117,6 +140,10 @@ export declare class CodexAdapter extends LlmAdapter {
|
|
|
117
140
|
* call — just because the TTL lapsed mid-turn.
|
|
118
141
|
*/
|
|
119
142
|
private discovered;
|
|
143
|
+
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
144
|
+
supportsFastTier(model: string): Promise<boolean>;
|
|
145
|
+
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
146
|
+
fastCapableModels(): Promise<string[]>;
|
|
120
147
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
121
148
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
122
149
|
private request;
|
package/lib/providers/codex.js
CHANGED
|
@@ -39,6 +39,14 @@ const CODEX_EFFORTS = [
|
|
|
39
39
|
const CODEX_DEFAULT_EFFORT = ReasoningEffortId('high');
|
|
40
40
|
/** Every gpt-5.x codex model accepts image input. */
|
|
41
41
|
const CODEX_MODALITIES = ['text', 'image'];
|
|
42
|
+
/**
|
|
43
|
+
* Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
|
|
44
|
+
* value for priority processing, mirroring codex-rs
|
|
45
|
+
* `ServiceTier::Fast.request_value()`. The legacy catalog spelling is the
|
|
46
|
+
* `additional_speed_tiers` entry "fast".
|
|
47
|
+
*/
|
|
48
|
+
export const CODEX_FAST_SERVICE_TIER = 'priority';
|
|
49
|
+
const CODEX_FAST_SPEED_TIER = 'fast';
|
|
42
50
|
/** Static codex flow facts for the OAuth flow engine. */
|
|
43
51
|
export const codexFlow = {
|
|
44
52
|
callbackPath: CODEX_CALLBACK_PATH,
|
|
@@ -286,6 +294,15 @@ export const CODEX_CLIENT_VERSION = '0.147.0';
|
|
|
286
294
|
function effortName(effort) {
|
|
287
295
|
return effort === 'xhigh' ? 'Extra High' : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
288
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Whether a catalog entry advertises the fast tier. Mirrors codex-rs
|
|
299
|
+
* `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
|
|
300
|
+
* wire value, or the legacy `additional_speed_tiers` "fast" entry.
|
|
301
|
+
*/
|
|
302
|
+
function supportsFastTier(entry) {
|
|
303
|
+
return (entry.service_tiers ?? []).some(tier => tier.id === CODEX_FAST_SERVICE_TIER)
|
|
304
|
+
|| (entry.additional_speed_tiers ?? []).includes(CODEX_FAST_SPEED_TIER);
|
|
305
|
+
}
|
|
289
306
|
/**
|
|
290
307
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
291
308
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
@@ -328,7 +345,7 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
328
345
|
&& efforts.some(effort => effort.id === ReasoningEffortId(entry.default_reasoning_level))
|
|
329
346
|
? ReasoningEffortId(entry.default_reasoning_level)
|
|
330
347
|
: undefined;
|
|
331
|
-
|
|
348
|
+
const model = {
|
|
332
349
|
id: entry.slug,
|
|
333
350
|
name: typeof entry.display_name === 'string' && entry.display_name.length > 0
|
|
334
351
|
? entry.display_name
|
|
@@ -343,7 +360,9 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
343
360
|
...efforts.length > 0
|
|
344
361
|
? { reasoning: { efforts, ...defaultEffort === undefined ? {} : { defaultEffort } } }
|
|
345
362
|
: {},
|
|
346
|
-
|
|
363
|
+
...supportsFastTier(entry) ? { fastTier: true } : {},
|
|
364
|
+
};
|
|
365
|
+
discovered.push(model);
|
|
347
366
|
}
|
|
348
367
|
discovered.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));
|
|
349
368
|
// An empty catalog from a 200 response means the backend gated us out (e.g.
|
|
@@ -354,6 +373,32 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
354
373
|
}
|
|
355
374
|
return discovered;
|
|
356
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* The Responses request body for one generation. A fast-tier request (the
|
|
378
|
+
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
379
|
+
* `service_tier: priority`; the tier field is omitted entirely otherwise,
|
|
380
|
+
* matching the CLI (it never sends an explicit standard tier).
|
|
381
|
+
*/
|
|
382
|
+
export function codexRequestBody(options, resolved, fast) {
|
|
383
|
+
return {
|
|
384
|
+
model: options.model,
|
|
385
|
+
instructions: resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
386
|
+
input: resolved.input,
|
|
387
|
+
...options.tools !== undefined && options.tools.length > 0
|
|
388
|
+
? { tools: toResponsesTools(options.tools) }
|
|
389
|
+
: {},
|
|
390
|
+
tool_choice: 'auto',
|
|
391
|
+
parallel_tool_calls: true,
|
|
392
|
+
...options.reasoningEffort !== undefined
|
|
393
|
+
? { reasoning: { effort: String(options.reasoningEffort), summary: 'auto' } }
|
|
394
|
+
: {},
|
|
395
|
+
store: false,
|
|
396
|
+
stream: true,
|
|
397
|
+
include: ['reasoning.encrypted_content'],
|
|
398
|
+
...options.sessionId !== undefined ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
399
|
+
...fast ? { service_tier: CODEX_FAST_SERVICE_TIER } : {},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
357
402
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
358
403
|
export class CodexAdapter extends LlmAdapter {
|
|
359
404
|
options;
|
|
@@ -423,6 +468,22 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
423
468
|
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
424
469
|
return models?.find(entry => entry.id === model);
|
|
425
470
|
}
|
|
471
|
+
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
472
|
+
async supportsFastTier(model) {
|
|
473
|
+
return (await this.discovered(model))?.fastTier === true;
|
|
474
|
+
}
|
|
475
|
+
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
476
|
+
async fastCapableModels() {
|
|
477
|
+
if (!this.options.discovery)
|
|
478
|
+
return [];
|
|
479
|
+
// Not logged in → no fast models, so the Speed toggle hides after logout
|
|
480
|
+
// (mirrors the listModels guard above).
|
|
481
|
+
const session = await this.options.tokens.peek();
|
|
482
|
+
if (session === undefined)
|
|
483
|
+
return [];
|
|
484
|
+
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
485
|
+
return (models ?? []).filter(model => model.fastTier === true).map(model => model.id);
|
|
486
|
+
}
|
|
426
487
|
async resolveModel(provider, model) {
|
|
427
488
|
// Discovered metadata (when discovery is on) wins over the static entry;
|
|
428
489
|
// the static entry wins over the built-in defaults.
|
|
@@ -465,24 +526,9 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
465
526
|
}
|
|
466
527
|
async request(options, session, signal) {
|
|
467
528
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
instructions: instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
472
|
-
input,
|
|
473
|
-
...options.tools !== undefined && options.tools.length > 0
|
|
474
|
-
? { tools: toResponsesTools(options.tools) }
|
|
475
|
-
: {},
|
|
476
|
-
tool_choice: 'auto',
|
|
477
|
-
parallel_tool_calls: true,
|
|
478
|
-
...options.reasoningEffort !== undefined
|
|
479
|
-
? { reasoning: { effort: String(options.reasoningEffort), summary: 'auto' } }
|
|
480
|
-
: {},
|
|
481
|
-
store: false,
|
|
482
|
-
stream: true,
|
|
483
|
-
include: ['reasoning.encrypted_content'],
|
|
484
|
-
...options.sessionId !== undefined ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
485
|
-
};
|
|
529
|
+
const fast = this.options.speedFor !== undefined
|
|
530
|
+
&& await this.options.speedFor(options.sessionId, options.model);
|
|
531
|
+
const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
|
|
486
532
|
return fetch(CODEX_API_URL, {
|
|
487
533
|
method: 'POST',
|
|
488
534
|
headers: {
|
|
@@ -175,6 +175,10 @@ export interface DiscoveredModel {
|
|
|
175
175
|
}[];
|
|
176
176
|
defaultEffort?: ReasoningEffortId;
|
|
177
177
|
};
|
|
178
|
+
/** Claude-specific: which extended-thinking wire shape this model accepts. */
|
|
179
|
+
thinkingType?: 'enabled' | 'adaptive';
|
|
180
|
+
/** Codex-specific: the catalog advertises a fast (priority) service tier. */
|
|
181
|
+
fastTier?: boolean;
|
|
178
182
|
}
|
|
179
183
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
180
184
|
export declare const DISCOVERY_TTL_MS: number;
|
|
@@ -16,7 +16,6 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
16
16
|
import { basename, join } from 'node:path';
|
|
17
17
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
18
18
|
import { AttachmentId } from '@deepseek-ai/dsh-attachment';
|
|
19
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
20
19
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
21
20
|
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
22
21
|
/** Endpoint the codex generation request is posted to. */
|
|
@@ -334,15 +333,9 @@ export function createImageGenerateTool(options) {
|
|
|
334
333
|
...refs.length > 0 ? { images: refs } : {},
|
|
335
334
|
...revisedPrompt === undefined ? {} : { revisedPrompt },
|
|
336
335
|
};
|
|
337
|
-
// Nested (Code Mode) dispatches
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
if (exec.parent !== undefined && refs.length > 0) {
|
|
341
|
-
exec.deferContext(createUserMessage({
|
|
342
|
-
content: imageGenerateContent(value),
|
|
343
|
-
source: { kind: 'plugin', plugin: 'dsh-plugin-subscriptions' },
|
|
344
|
-
}));
|
|
345
|
-
}
|
|
336
|
+
// Nested (Code Mode) dispatches need no defer here: the harness's code
|
|
337
|
+
// mode already defers any image-bearing sub-result as a user message, so
|
|
338
|
+
// deferring again would inject the same attachment twice.
|
|
346
339
|
return value;
|
|
347
340
|
},
|
|
348
341
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscriptions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Use ChatGPT (Codex), Claude, and Grok (X Premium) subscriptions as DeepSeek Harness LLM providers, with OAuth login from the web Settings page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
"@deepseek-ai/dsh-client-locale": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/locale",
|
|
71
71
|
"@deepseek-ai/dsh-client-runtime": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/runtime",
|
|
72
72
|
"@deepseek-ai/dsh-client-ui-settings": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-settings",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-conversation": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-conversation",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-commands": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-commands",
|
|
73
75
|
"@deepseek-ai/dsh-client-ui-slots": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/client/ui-slots",
|
|
74
76
|
"@deepseek-ai/dsh-home-paths": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/util/home-paths",
|
|
75
77
|
"@deepseek-ai/dsh-host-apiproxy": "link:/Users/v1ki/Documents/projs/source/deepseek-harness/packages/host/apiproxy",
|