dsh-plugin-subscriptions 0.4.0 → 0.4.2
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 +14 -6
- package/README.zh.md +14 -6
- 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/client/SubscriptionsSection.js +6 -1
- package/lib/client.js +5 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +498 -77
- package/lib/providers/catalog-store.js +4 -0
- package/lib/providers/claude.d.ts +35 -3
- package/lib/providers/claude.js +181 -27
- package/lib/providers/common.d.ts +2 -0
- package/lib/tools/image-generate.d.ts +55 -19
- package/lib/tools/image-generate.js +130 -31
- package/package.json +6 -8
|
@@ -71,6 +71,9 @@ 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;
|
|
74
77
|
return {
|
|
75
78
|
id: raw.id,
|
|
76
79
|
name: raw.name,
|
|
@@ -78,6 +81,7 @@ function sanitizeModel(value) {
|
|
|
78
81
|
...raw.contextWindow === undefined ? {} : { contextWindow: raw.contextWindow },
|
|
79
82
|
...raw.priority === undefined ? {} : { priority: raw.priority },
|
|
80
83
|
...reasoning === undefined ? {} : { reasoning },
|
|
84
|
+
...thinkingType === undefined ? {} : { thinkingType: thinkingType },
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
/**
|
|
@@ -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,39 @@ 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
|
+
const CLAUDE_CLI_USER_AGENT = `claude-cli/${detectClaudeVersion()} (external, cli)`;
|
|
40
|
+
export const CLAUDE_BETA_FALLBACK = [
|
|
41
|
+
'claude-code-20250219',
|
|
42
|
+
'oauth-2025-04-20',
|
|
43
|
+
'interleaved-thinking-2025-05-14',
|
|
44
|
+
'context-management-2025-06-27',
|
|
45
|
+
'effort-2025-11-24',
|
|
46
|
+
'compact-2026-01-12',
|
|
47
|
+
'files-api-2025-04-14',
|
|
48
|
+
].join(',');
|
|
49
|
+
const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
|
|
28
50
|
/** Static claude flow facts for the OAuth flow engine. */
|
|
29
51
|
export const claudeFlow = {
|
|
30
52
|
callbackPath: CLAUDE_CALLBACK_PATH,
|
|
31
53
|
// The redirect URI embeds the port, so it must be an ephemeral one.
|
|
32
|
-
listen: { host: '
|
|
54
|
+
listen: { host: '127.0.0.1', ports: [0] },
|
|
33
55
|
buildAuthorizeUrl({ redirectUri, state, pkce }) {
|
|
34
56
|
const params = new URLSearchParams({
|
|
35
57
|
code: 'true',
|
|
36
58
|
client_id: CLAUDE_CLIENT_ID,
|
|
37
59
|
response_type: 'code',
|
|
38
|
-
redirect_uri:
|
|
60
|
+
redirect_uri: 'https://platform.claude.com/oauth/code/callback',
|
|
39
61
|
scope: CLAUDE_SCOPE,
|
|
40
62
|
code_challenge: pkce.challenge,
|
|
41
63
|
code_challenge_method: 'S256',
|
|
@@ -236,24 +258,88 @@ export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
|
|
|
236
258
|
}
|
|
237
259
|
return { supported: true, windows };
|
|
238
260
|
}
|
|
261
|
+
function claudeThinkingType(capabilities) {
|
|
262
|
+
const types = capabilities?.thinking?.types;
|
|
263
|
+
if (types?.enabled?.supported === true)
|
|
264
|
+
return 'enabled';
|
|
265
|
+
if (types?.adaptive?.supported === true)
|
|
266
|
+
return 'adaptive';
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
/** Effort levels in display order; a model exposes only the ones it advertises as supported. */
|
|
270
|
+
const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
271
|
+
function claudeReasoning(capabilities) {
|
|
272
|
+
const effort = capabilities?.effort;
|
|
273
|
+
if (effort?.supported !== true)
|
|
274
|
+
return undefined;
|
|
275
|
+
const efforts = CLAUDE_EFFORT_LEVELS
|
|
276
|
+
.filter(level => effort[level]?.supported === true)
|
|
277
|
+
.map(level => ({ id: ReasoningEffortId(level), name: level[0].toUpperCase() + level.slice(1) }));
|
|
278
|
+
return efforts.length > 0 ? { efforts } : undefined;
|
|
279
|
+
}
|
|
280
|
+
/** Fetch the live model catalog from the subscription endpoint. */
|
|
281
|
+
export async function fetchClaudeModels(session, fetchFn = fetch) {
|
|
282
|
+
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
283
|
+
headers: {
|
|
284
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
285
|
+
'anthropic-version': '2023-06-01',
|
|
286
|
+
'user-agent': CLAUDE_CLI_USER_AGENT,
|
|
287
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
288
|
+
'accept': 'application/json',
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
if (!response.ok)
|
|
292
|
+
throw await httpLlmError(response, 'claude models API');
|
|
293
|
+
const payload = await response.json();
|
|
294
|
+
if (!Array.isArray(payload.data)) {
|
|
295
|
+
throw new Error('claude models API returned an invalid catalog');
|
|
296
|
+
}
|
|
297
|
+
const models = payload.data
|
|
298
|
+
.filter((m) => typeof m.id === 'string')
|
|
299
|
+
.map((m) => {
|
|
300
|
+
const thinkingType = claudeThinkingType(m.capabilities);
|
|
301
|
+
const reasoning = claudeReasoning(m.capabilities);
|
|
302
|
+
return {
|
|
303
|
+
id: m.id,
|
|
304
|
+
name: m.display_name ?? m.id,
|
|
305
|
+
...thinkingType === undefined ? {} : { thinkingType },
|
|
306
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
if (models.length === 0) {
|
|
310
|
+
throw new Error('claude models API returned an empty catalog');
|
|
311
|
+
}
|
|
312
|
+
return models;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Claude Code's own SDK retry shape: exponential backoff starting at 1s,
|
|
316
|
+
* doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
|
|
317
|
+
* count of retries after the first attempt (Claude Code defaults to 10).
|
|
318
|
+
*/
|
|
319
|
+
const CLAUDE_RETRY_INITIAL_DELAY_MS = 1_000;
|
|
320
|
+
const CLAUDE_RETRY_MAX_DELAY_MS = 60_000;
|
|
321
|
+
const CLAUDE_RETRY_JITTER_RATIO = 0.2;
|
|
239
322
|
/** The Claude 4.5 family accepts image input. */
|
|
240
323
|
const CLAUDE_MODALITIES = ['text', 'image'];
|
|
241
324
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
242
325
|
export class ClaudeAdapter extends LlmAdapter {
|
|
243
326
|
options;
|
|
327
|
+
catalog;
|
|
244
328
|
constructor(options) {
|
|
245
329
|
super();
|
|
246
330
|
this.options = options;
|
|
331
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
247
332
|
}
|
|
248
|
-
|
|
249
|
-
return
|
|
333
|
+
async fetchCatalog() {
|
|
334
|
+
return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
250
335
|
}
|
|
251
|
-
async
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
336
|
+
async discovered(model) {
|
|
337
|
+
if (!this.options.discovery)
|
|
338
|
+
return undefined;
|
|
339
|
+
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
340
|
+
return models?.find(entry => entry.id === model);
|
|
341
|
+
}
|
|
342
|
+
staticModels(provider) {
|
|
257
343
|
return this.options.models.map(model => ({
|
|
258
344
|
provider,
|
|
259
345
|
id: model.id,
|
|
@@ -261,18 +347,61 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
261
347
|
inputModalities: model.inputModalities ?? CLAUDE_MODALITIES,
|
|
262
348
|
}));
|
|
263
349
|
}
|
|
264
|
-
|
|
350
|
+
providerInfo(provider) {
|
|
351
|
+
return { id: provider, name: 'Claude (Subscription)' };
|
|
352
|
+
}
|
|
353
|
+
providerRetryPolicy(provider) {
|
|
354
|
+
if (this.options.maxRetries === undefined)
|
|
355
|
+
return undefined;
|
|
356
|
+
return resolveRetryPolicy({
|
|
357
|
+
mode: 'normal',
|
|
358
|
+
maxRetries: this.options.maxRetries,
|
|
359
|
+
backoff: {
|
|
360
|
+
initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
|
|
361
|
+
maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
|
|
362
|
+
jitterRatio: CLAUDE_RETRY_JITTER_RATIO,
|
|
363
|
+
},
|
|
364
|
+
}, `claude: provider "${provider}" retryPolicy`);
|
|
365
|
+
}
|
|
366
|
+
async listModels(provider) {
|
|
367
|
+
if (await this.options.tokens.peek() === undefined)
|
|
368
|
+
return [];
|
|
369
|
+
if (!this.options.discovery)
|
|
370
|
+
return this.staticModels(provider);
|
|
371
|
+
try {
|
|
372
|
+
const models = await this.catalog.get(() => this.fetchCatalog());
|
|
373
|
+
return models.map(model => ({
|
|
374
|
+
provider,
|
|
375
|
+
id: model.id,
|
|
376
|
+
name: model.name,
|
|
377
|
+
inputModalities: CLAUDE_MODALITIES,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof LlmError
|
|
382
|
+
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
|
|
383
|
+
return [];
|
|
384
|
+
if (error instanceof LlmError && error.code === 'AUTH')
|
|
385
|
+
this.catalog.invalidate();
|
|
386
|
+
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
387
|
+
return this.staticModels(provider);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async resolveModel(provider, model) {
|
|
391
|
+
const disc = await this.discovered(model);
|
|
265
392
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
266
|
-
|
|
393
|
+
const reasoning = disc?.reasoning;
|
|
394
|
+
return {
|
|
267
395
|
provider,
|
|
268
396
|
id: model,
|
|
269
|
-
name: configured?.name ?? model,
|
|
397
|
+
name: disc?.name ?? configured?.name ?? model,
|
|
270
398
|
inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
|
|
271
|
-
context: {
|
|
399
|
+
context: {
|
|
400
|
+
contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW,
|
|
401
|
+
},
|
|
272
402
|
defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
403
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
404
|
+
};
|
|
276
405
|
}
|
|
277
406
|
async *stream(options) {
|
|
278
407
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -280,7 +409,6 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
280
409
|
let session = await this.options.tokens.session();
|
|
281
410
|
let response = await this.request(options, session, watchdog.signal);
|
|
282
411
|
if (response.status === 401) {
|
|
283
|
-
// One forced refresh + retry on an unexpired-but-rejected token.
|
|
284
412
|
session = await this.options.tokens.session(true);
|
|
285
413
|
response = await this.request(options, session, watchdog.signal);
|
|
286
414
|
}
|
|
@@ -298,18 +426,44 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
298
426
|
watchdog.stop();
|
|
299
427
|
}
|
|
300
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
|
|
431
|
+
* models default to `display: 'omitted'`, which returns thinking blocks with
|
|
432
|
+
* an empty `thinking` field — without this override the "Think" panel would
|
|
433
|
+
* always render empty even though real reasoning (and billed thinking_tokens)
|
|
434
|
+
* ran.
|
|
435
|
+
*/
|
|
436
|
+
thinkingParam(thinkingType, maxTokens) {
|
|
437
|
+
if (thinkingType === 'adaptive')
|
|
438
|
+
return { type: 'adaptive', display: 'summarized' };
|
|
439
|
+
if (thinkingType === 'enabled') {
|
|
440
|
+
const budget = Math.min(Math.max(1_024, Math.floor(maxTokens * 0.5)), maxTokens - 100);
|
|
441
|
+
if (budget < 1_024)
|
|
442
|
+
return undefined;
|
|
443
|
+
return { type: 'enabled', budget_tokens: budget, display: 'summarized' };
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
301
447
|
async request(options, session, signal) {
|
|
302
448
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
449
|
+
const maxTokens = options.maxTokens
|
|
450
|
+
?? this.options.models.find(entry => entry.id === options.model)?.maxTokens
|
|
451
|
+
?? CLAUDE_DEFAULT_MAX_TOKENS;
|
|
452
|
+
const disc = await this.discovered(options.model);
|
|
453
|
+
const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
|
|
454
|
+
const effort = options.reasoningEffort !== undefined && disc?.reasoning !== undefined
|
|
455
|
+
? { output_config: { effort: String(options.reasoningEffort) } }
|
|
456
|
+
: {};
|
|
303
457
|
const body = {
|
|
304
458
|
model: options.model,
|
|
305
|
-
max_tokens:
|
|
306
|
-
?? this.options.models.find(entry => entry.id === options.model)?.maxTokens
|
|
307
|
-
?? CLAUDE_DEFAULT_MAX_TOKENS,
|
|
459
|
+
max_tokens: maxTokens,
|
|
308
460
|
system: toAnthropicSystem(options.system, messages),
|
|
309
461
|
messages: toAnthropicMessages(messages),
|
|
310
462
|
...options.tools !== undefined && options.tools.length > 0
|
|
311
463
|
? { tools: toAnthropicTools(options.tools) }
|
|
312
464
|
: {},
|
|
465
|
+
...thinking === undefined ? {} : { thinking },
|
|
466
|
+
...effort,
|
|
313
467
|
stream: true,
|
|
314
468
|
...options.sessionId !== undefined ? { metadata: { user_id: String(options.sessionId) } } : {},
|
|
315
469
|
};
|
|
@@ -175,6 +175,8 @@ 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';
|
|
178
180
|
}
|
|
179
181
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
180
182
|
export declare const DISCOVERY_TTL_MS: number;
|
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `image_generate` tool: generate images through
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* `image_generate` tool: generate images through a subscription image
|
|
3
|
+
* endpoint, save them under the harness home, and — when the deployment
|
|
4
|
+
* mounts an attachment store and the calling route declares image input —
|
|
5
|
+
* also commit the bytes as durable attachments so the images render inline
|
|
6
|
+
* and enter model context (the same path `read_image` uses).
|
|
7
|
+
*
|
|
8
|
+
* Provider selection: the `provider` argument names the preferred provider
|
|
9
|
+
* (default `gpt`, i.e. the ChatGPT/Codex subscription serving gpt-image-2 —
|
|
10
|
+
* mirrors codex-rs `codex-api/src/images.rs`); when the preferred one is
|
|
11
|
+
* logged out the other serves as fallback (`grok` is grok-imagine-image-2.0
|
|
12
|
+
* via `api.x.ai/v1/images/generations` with `response_format: 'b64_json'`).
|
|
13
|
+
* Both answer the OpenAI images shape (`data[].b64_json`).
|
|
10
14
|
*/
|
|
11
15
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
12
16
|
import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
|
|
13
17
|
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
14
|
-
import type { CodexSession } from '../auth/store.js';
|
|
18
|
+
import type { CodexSession, GrokSession } from '../auth/store.js';
|
|
15
19
|
import { TokenManager } from '../providers/common.js';
|
|
16
20
|
import type { FetchFn } from '../providers/common.js';
|
|
17
|
-
/** Endpoint the generation request is posted to. */
|
|
21
|
+
/** Endpoint the codex generation request is posted to. */
|
|
18
22
|
export declare const IMAGE_GENERATE_URL = "https://chatgpt.com/backend-api/codex/images/generations";
|
|
19
23
|
/** The image model the codex subscription endpoint serves. */
|
|
20
24
|
export declare const IMAGE_GENERATE_MODEL = "gpt-image-2";
|
|
25
|
+
/** Endpoint the grok generation request is posted to. */
|
|
26
|
+
export declare const GROK_IMAGE_GENERATE_URL = "https://api.x.ai/v1/images/generations";
|
|
27
|
+
/** The image model the grok subscription endpoint serves. */
|
|
28
|
+
export declare const GROK_IMAGE_GENERATE_MODEL = "grok-imagine-image-2.0";
|
|
21
29
|
/** Dependencies of the `image_generate` tool. */
|
|
22
30
|
export interface ImageGenerateToolOptions {
|
|
23
|
-
/** Codex session source;
|
|
24
|
-
|
|
31
|
+
/** Codex session source; the default preferred provider (`provider: 'gpt'`). */
|
|
32
|
+
codexTokens?: TokenManager<CodexSession>;
|
|
33
|
+
/** Grok session source; preferred when the call passes `provider: 'grok'`. */
|
|
34
|
+
grokTokens?: TokenManager<GrokSession>;
|
|
25
35
|
/** Fetch implementation (injectable for tests). */
|
|
26
36
|
fetchFn?: FetchFn;
|
|
27
37
|
/** Directory override for saved images (defaults under the harness home). */
|
|
@@ -38,15 +48,33 @@ export interface ImageGenerateRequestBody {
|
|
|
38
48
|
size?: string;
|
|
39
49
|
quality?: string;
|
|
40
50
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
* prompt the schema DSL cannot express).
|
|
44
|
-
*/
|
|
45
|
-
export declare function buildImageGenerateBody(args: {
|
|
51
|
+
/** The tool's own argument shape, shared by both provider body builders. */
|
|
52
|
+
export interface ImageGenerateArgs {
|
|
46
53
|
prompt: string;
|
|
47
54
|
size?: '1024x1024' | '1024x1536' | '1536x1024' | 'auto';
|
|
48
55
|
quality?: 'low' | 'medium' | 'high' | 'auto';
|
|
49
|
-
|
|
56
|
+
/** Preferred provider; the other one serves when the preferred is logged out. */
|
|
57
|
+
provider?: 'gpt' | 'grok';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Assemble the codex request body from tool arguments (hand-checks the
|
|
61
|
+
* non-empty prompt the schema DSL cannot express).
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildImageGenerateBody(args: ImageGenerateArgs): ImageGenerateRequestBody;
|
|
64
|
+
/** The wire request body for one grok generation call. */
|
|
65
|
+
export interface GrokImageGenerateRequestBody {
|
|
66
|
+
prompt: string;
|
|
67
|
+
model: string;
|
|
68
|
+
response_format: 'b64_json';
|
|
69
|
+
aspect_ratio?: string;
|
|
70
|
+
quality?: 'low' | 'medium';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Assemble the grok request body from the same tool arguments: `size` maps
|
|
74
|
+
* onto the nearest `aspect_ratio`, and `quality` folds into grok's low/medium
|
|
75
|
+
* pair (`high` → `medium`, `auto` → provider default).
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildGrokImageGenerateBody(args: ImageGenerateArgs): GrokImageGenerateRequestBody;
|
|
50
78
|
/** One generated image decoded from the response. */
|
|
51
79
|
export interface GeneratedImage {
|
|
52
80
|
/** PNG bytes. */
|
|
@@ -59,8 +87,16 @@ export interface GeneratedImage {
|
|
|
59
87
|
* payload carries no usable `b64_json` entries.
|
|
60
88
|
*/
|
|
61
89
|
export declare function parseImageGenerateResponse(payload: unknown): GeneratedImage[];
|
|
62
|
-
/** Directory the generated
|
|
90
|
+
/** Directory the generated image files are written to. */
|
|
63
91
|
export declare function imagesDirectory(): string;
|
|
92
|
+
/** Media types the attachment store accepts and this tool can produce. */
|
|
93
|
+
export type GeneratedImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp';
|
|
94
|
+
/**
|
|
95
|
+
* Sniff a generated image's media type from its magic bytes (codex serves
|
|
96
|
+
* PNG; grok's format is undocumented, so trust the bytes). Unrecognized data
|
|
97
|
+
* defaults to PNG, matching the historical behavior.
|
|
98
|
+
*/
|
|
99
|
+
export declare function sniffImageMediaType(data: Buffer): GeneratedImageMediaType;
|
|
64
100
|
/**
|
|
65
101
|
* Build the `image_generate` tool definition.
|
|
66
102
|
* @param options - codex session source, fetch implementation, and image directory.
|