dsh-plugin-subscriptions 0.3.1 → 0.4.1
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 +11 -2
- package/README.zh.md +11 -2
- package/lib/auth/rpc.d.ts +14 -0
- package/lib/auth/rpc.js +18 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +178 -0
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +10 -0
- package/lib/client/locales.d.ts +6 -0
- package/lib/client/locales.js +6 -0
- package/lib/client.js +282 -54
- package/lib/client.js.map +1 -1
- package/lib/index.js +408 -41
- package/lib/providers/codex.d.ts +5 -2
- package/lib/providers/codex.js +34 -4
- package/lib/tools/image-generate.d.ts +55 -19
- package/lib/tools/image-generate.js +130 -31
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +255 -0
- package/package.json +8 -6
package/lib/providers/codex.js
CHANGED
|
@@ -189,8 +189,31 @@ export function isCodexPermanentRefreshError(error) {
|
|
|
189
189
|
&& PERMANENT_REFRESH_CODES.has(error.oauthCode);
|
|
190
190
|
}
|
|
191
191
|
export const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
192
|
+
/** Seconds of the canonical 5-hour session and 7-day weekly windows. */
|
|
193
|
+
const SESSION_WINDOW_SECONDS = 5 * 60 * 60;
|
|
194
|
+
const WEEKLY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
|
|
195
|
+
/** Whether a reported duration approximately matches the expected window length. */
|
|
196
|
+
function matchesWindow(seconds, expected) {
|
|
197
|
+
return seconds >= expected * 0.95 && seconds <= expected * 1.05;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Classify a wham/usage window by its reported duration. The backend has been
|
|
201
|
+
* observed to place the weekly lane in `primary_window` with no secondary
|
|
202
|
+
* window, so slot position alone is unreliable; the caller's positional
|
|
203
|
+
* fallback applies only when the duration is absent.
|
|
204
|
+
*/
|
|
205
|
+
function codexWindowKind(window, fallback) {
|
|
206
|
+
const seconds = window.limit_window_seconds;
|
|
207
|
+
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0)
|
|
208
|
+
return fallback;
|
|
209
|
+
if (matchesWindow(seconds, SESSION_WINDOW_SECONDS))
|
|
210
|
+
return 'session';
|
|
211
|
+
if (matchesWindow(seconds, WEEKLY_WINDOW_SECONDS))
|
|
212
|
+
return 'weekly';
|
|
213
|
+
return 'other';
|
|
214
|
+
}
|
|
192
215
|
/** Map one wham/usage window into a {@link UsageWindow}; undefined when unusable. */
|
|
193
|
-
function codexUsageWindow(value,
|
|
216
|
+
function codexUsageWindow(value, fallbackKind) {
|
|
194
217
|
if (typeof value !== 'object' || value === null)
|
|
195
218
|
return undefined;
|
|
196
219
|
const window = value;
|
|
@@ -203,13 +226,20 @@ function codexUsageWindow(value, kind) {
|
|
|
203
226
|
else if (typeof window.reset_after_seconds === 'number' && window.reset_after_seconds > 0) {
|
|
204
227
|
resetsAt = Date.now() + window.reset_after_seconds * 1000;
|
|
205
228
|
}
|
|
206
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
kind: codexWindowKind(window, fallbackKind),
|
|
231
|
+
usedPercent: window.used_percent,
|
|
232
|
+
...resetsAt === undefined ? {} : { resetsAt },
|
|
233
|
+
};
|
|
207
234
|
}
|
|
208
235
|
/**
|
|
209
236
|
* Fetch the codex subscription usage from the ChatGPT backend wham/usage
|
|
210
237
|
* endpoint (the source of the codex CLI `/status` rate-limit lines). The
|
|
211
|
-
*
|
|
212
|
-
*
|
|
238
|
+
* windows are classified by their reported duration (`limit_window_seconds`)
|
|
239
|
+
* rather than by slot, since the backend has been observed to report the
|
|
240
|
+
* weekly lane as `primary_window` without a secondary window; slot order is
|
|
241
|
+
* kept only as a fallback when the duration is absent. The lookup itself
|
|
242
|
+
* consumes no rate-limit budget.
|
|
213
243
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
214
244
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
215
245
|
* @param signal - caller cancellation from the RPC transport.
|
|
@@ -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.
|
|
@@ -1,12 +1,16 @@
|
|
|
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 { mkdir, writeFile } from 'node:fs/promises';
|
|
12
16
|
import { basename, join } from 'node:path';
|
|
@@ -15,13 +19,17 @@ import { AttachmentId } from '@deepseek-ai/dsh-attachment';
|
|
|
15
19
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
16
20
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
17
21
|
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
18
|
-
/** Endpoint the generation request is posted to. */
|
|
22
|
+
/** Endpoint the codex generation request is posted to. */
|
|
19
23
|
export const IMAGE_GENERATE_URL = 'https://chatgpt.com/backend-api/codex/images/generations';
|
|
20
24
|
/** The image model the codex subscription endpoint serves. */
|
|
21
25
|
export const IMAGE_GENERATE_MODEL = 'gpt-image-2';
|
|
26
|
+
/** Endpoint the grok generation request is posted to. */
|
|
27
|
+
export const GROK_IMAGE_GENERATE_URL = 'https://api.x.ai/v1/images/generations';
|
|
28
|
+
/** The image model the grok subscription endpoint serves. */
|
|
29
|
+
export const GROK_IMAGE_GENERATE_MODEL = 'grok-imagine-image-2.0';
|
|
22
30
|
/**
|
|
23
|
-
* Assemble the request body from tool arguments (hand-checks the
|
|
24
|
-
* prompt the schema DSL cannot express).
|
|
31
|
+
* Assemble the codex request body from tool arguments (hand-checks the
|
|
32
|
+
* non-empty prompt the schema DSL cannot express).
|
|
25
33
|
*/
|
|
26
34
|
export function buildImageGenerateBody(args) {
|
|
27
35
|
const prompt = args.prompt.trim();
|
|
@@ -34,6 +42,33 @@ export function buildImageGenerateBody(args) {
|
|
|
34
42
|
...args.quality === undefined ? {} : { quality: args.quality },
|
|
35
43
|
};
|
|
36
44
|
}
|
|
45
|
+
/** The codex `size` values mapped onto grok aspect ratios. */
|
|
46
|
+
const GROK_ASPECT_RATIOS = {
|
|
47
|
+
'1024x1024': '1:1',
|
|
48
|
+
'1024x1536': '2:3',
|
|
49
|
+
'1536x1024': '3:2',
|
|
50
|
+
'auto': 'auto',
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Assemble the grok request body from the same tool arguments: `size` maps
|
|
54
|
+
* onto the nearest `aspect_ratio`, and `quality` folds into grok's low/medium
|
|
55
|
+
* pair (`high` → `medium`, `auto` → provider default).
|
|
56
|
+
*/
|
|
57
|
+
export function buildGrokImageGenerateBody(args) {
|
|
58
|
+
const prompt = args.prompt.trim();
|
|
59
|
+
if (prompt.length === 0)
|
|
60
|
+
throw new Error('image_generate: prompt must be a non-empty string');
|
|
61
|
+
const quality = args.quality === 'low' ? 'low'
|
|
62
|
+
: args.quality === 'medium' || args.quality === 'high' ? 'medium'
|
|
63
|
+
: undefined;
|
|
64
|
+
return {
|
|
65
|
+
prompt,
|
|
66
|
+
model: GROK_IMAGE_GENERATE_MODEL,
|
|
67
|
+
response_format: 'b64_json',
|
|
68
|
+
...args.size === undefined ? {} : { aspect_ratio: GROK_ASPECT_RATIOS[args.size] },
|
|
69
|
+
...quality === undefined ? {} : { quality },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
37
72
|
/**
|
|
38
73
|
* Parse the generations response into decodable images. Throws when the
|
|
39
74
|
* payload carries no usable `b64_json` entries.
|
|
@@ -59,14 +94,33 @@ export function parseImageGenerateResponse(payload) {
|
|
|
59
94
|
throw new Error('image_generate: the response carried no image data');
|
|
60
95
|
return images;
|
|
61
96
|
}
|
|
62
|
-
/** Directory the generated
|
|
97
|
+
/** Directory the generated image files are written to. */
|
|
63
98
|
export function imagesDirectory() {
|
|
64
99
|
return dshHomePath('plugins', 'subscriptions', 'images');
|
|
65
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Sniff a generated image's media type from its magic bytes (codex serves
|
|
103
|
+
* PNG; grok's format is undocumented, so trust the bytes). Unrecognized data
|
|
104
|
+
* defaults to PNG, matching the historical behavior.
|
|
105
|
+
*/
|
|
106
|
+
export function sniffImageMediaType(data) {
|
|
107
|
+
if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
|
|
108
|
+
return 'image/jpeg';
|
|
109
|
+
if (data.length >= 12 && data.toString('latin1', 0, 4) === 'RIFF' && data.toString('latin1', 8, 12) === 'WEBP') {
|
|
110
|
+
return 'image/webp';
|
|
111
|
+
}
|
|
112
|
+
return 'image/png';
|
|
113
|
+
}
|
|
114
|
+
/** File extension for one sniffed media type. */
|
|
115
|
+
const MEDIA_TYPE_EXTENSIONS = {
|
|
116
|
+
'image/png': 'png',
|
|
117
|
+
'image/jpeg': 'jpg',
|
|
118
|
+
'image/webp': 'webp',
|
|
119
|
+
};
|
|
66
120
|
/** Timestamped, collision-safe file name for one generated image. */
|
|
67
|
-
function imageFileName(index) {
|
|
121
|
+
function imageFileName(index, mediaType) {
|
|
68
122
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
69
|
-
return `image-${stamp}-${Math.random().toString(36).slice(2, 8)}-${index}
|
|
123
|
+
return `image-${stamp}-${Math.random().toString(36).slice(2, 8)}-${index}.${MEDIA_TYPE_EXTENSIONS[mediaType]}`;
|
|
70
124
|
}
|
|
71
125
|
/** Bound a call-card title's prompt. */
|
|
72
126
|
function truncate(text, max = 60) {
|
|
@@ -127,7 +181,10 @@ function imageGenerateText(value) {
|
|
|
127
181
|
export function createImageGenerateTool(options) {
|
|
128
182
|
return defineTool({
|
|
129
183
|
name: 'image_generate',
|
|
130
|
-
description: 'Generate an image with the ChatGPT subscription (gpt-image-2)
|
|
184
|
+
description: 'Generate an image with the ChatGPT subscription (gpt-image-2) or the Grok '
|
|
185
|
+
+ 'subscription (grok-imagine-image-2.0) and save it as an image file. The `provider` '
|
|
186
|
+
+ 'parameter picks the preferred provider (default gpt); when the preferred one is logged '
|
|
187
|
+
+ 'out the other serves as fallback. '
|
|
131
188
|
+ 'Returns the saved file paths; on image-capable models the image itself is attached.',
|
|
132
189
|
parameters: {
|
|
133
190
|
prompt: { type: 'string', required: true, description: 'What the image should show.' },
|
|
@@ -141,6 +198,11 @@ export function createImageGenerateTool(options) {
|
|
|
141
198
|
enum: ['low', 'medium', 'high', 'auto'],
|
|
142
199
|
description: 'Rendering quality; omit for the provider default.',
|
|
143
200
|
},
|
|
201
|
+
provider: {
|
|
202
|
+
type: 'string',
|
|
203
|
+
enum: ['gpt', 'grok'],
|
|
204
|
+
description: 'Preferred provider (default gpt); the other one serves as fallback when the preferred is logged out.',
|
|
205
|
+
},
|
|
144
206
|
},
|
|
145
207
|
output: {
|
|
146
208
|
schema: {
|
|
@@ -180,30 +242,67 @@ export function createImageGenerateTool(options) {
|
|
|
180
242
|
content: result.content.filter(block => block.type === 'text'),
|
|
181
243
|
}),
|
|
182
244
|
async execute(args, exec) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
245
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
246
|
+
// Provider selection: the preferred provider (default gpt) when logged
|
|
247
|
+
// in, the other one as the fallback. A configured-but-logged-out manager
|
|
248
|
+
// still resolves through `session()` below so the standard log-in hint
|
|
249
|
+
// surfaces.
|
|
250
|
+
const preferGrok = args.provider === 'grok';
|
|
251
|
+
const codexReady = options.codexTokens !== undefined && await options.codexTokens.hasSession();
|
|
252
|
+
const grokReady = options.grokTokens !== undefined && await options.grokTokens.hasSession();
|
|
253
|
+
const useGrok = preferGrok ? grokReady : grokReady && !codexReady;
|
|
254
|
+
const useCodex = !useGrok && codexReady;
|
|
255
|
+
let response;
|
|
256
|
+
if (useCodex && options.codexTokens !== undefined) {
|
|
257
|
+
const session = await options.codexTokens.session();
|
|
258
|
+
response = await fetchFn(IMAGE_GENERATE_URL, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
262
|
+
'chatgpt-account-id': session.accountId,
|
|
263
|
+
'originator': 'codex_cli_rs',
|
|
264
|
+
'content-type': 'application/json',
|
|
265
|
+
'accept': 'application/json',
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify(buildImageGenerateBody(args)),
|
|
268
|
+
signal: exec.signal,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else if (useGrok && options.grokTokens !== undefined) {
|
|
272
|
+
const session = await options.grokTokens.session();
|
|
273
|
+
response = await fetchFn(GROK_IMAGE_GENERATE_URL, {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
headers: {
|
|
276
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
277
|
+
'content-type': 'application/json',
|
|
278
|
+
'accept': 'application/json',
|
|
279
|
+
},
|
|
280
|
+
body: JSON.stringify(buildGrokImageGenerateBody(args)),
|
|
281
|
+
signal: exec.signal,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const manager = preferGrok
|
|
286
|
+
? options.grokTokens ?? options.codexTokens
|
|
287
|
+
: options.codexTokens ?? options.grokTokens;
|
|
288
|
+
if (manager === undefined)
|
|
289
|
+
throw new Error('image_generate: no image provider is configured');
|
|
290
|
+
await manager.session(); // logged out: throws the provider's log-in hint
|
|
291
|
+
throw new Error('image_generate: no image provider is logged in');
|
|
292
|
+
}
|
|
197
293
|
if (!response.ok)
|
|
198
294
|
throw await httpLlmError(response, 'image_generate');
|
|
199
295
|
const images = parseImageGenerateResponse(await response.json());
|
|
200
296
|
const directory = options.imagesDir ?? imagesDirectory();
|
|
201
297
|
await mkdir(directory, { recursive: true });
|
|
202
298
|
const paths = [];
|
|
299
|
+
const mediaTypes = [];
|
|
203
300
|
for (const [index, image] of images.entries()) {
|
|
204
|
-
const
|
|
301
|
+
const mediaType = sniffImageMediaType(image.data);
|
|
302
|
+
const path = join(directory, imageFileName(index, mediaType));
|
|
205
303
|
await writeFile(path, image.data);
|
|
206
304
|
paths.push(path);
|
|
305
|
+
mediaTypes.push(mediaType);
|
|
207
306
|
}
|
|
208
307
|
// Inline display requires durable attachment references, and those may
|
|
209
308
|
// only enter session history on a route that declares image input.
|
|
@@ -216,7 +315,7 @@ export function createImageGenerateTool(options) {
|
|
|
216
315
|
for (const [index, image] of images.entries()) {
|
|
217
316
|
const ref = await attachments.saveImage({
|
|
218
317
|
data: image.data,
|
|
219
|
-
mediaType:
|
|
318
|
+
mediaType: mediaTypes[index],
|
|
220
319
|
name: basename(paths[index]),
|
|
221
320
|
});
|
|
222
321
|
refs.push({
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `video_generate` tool: generate videos through the grok subscription's
|
|
3
|
+
* Imagine video endpoint and save them as MP4 files under the harness home.
|
|
4
|
+
* The xAI API is asynchronous: POST `/v1/videos/generations` returns a
|
|
5
|
+
* `request_id`, GET `/v1/videos/{request_id}` is polled until the status
|
|
6
|
+
* leaves `pending`, and the completed response carries a temporary MP4 URL
|
|
7
|
+
* that is downloaded promptly (the URL expires). The canonical result is the
|
|
8
|
+
* saved file path; videos have no attachment surface, so the result stays
|
|
9
|
+
* text-only (unlike image_generate).
|
|
10
|
+
*/
|
|
11
|
+
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
12
|
+
import type { GrokSession } from '../auth/store.js';
|
|
13
|
+
import { TokenManager } from '../providers/common.js';
|
|
14
|
+
import type { FetchFn } from '../providers/common.js';
|
|
15
|
+
/** Endpoint the generation request is posted to. */
|
|
16
|
+
export declare const VIDEO_GENERATE_URL = "https://api.x.ai/v1/videos/generations";
|
|
17
|
+
/** The video model the grok subscription endpoint serves. */
|
|
18
|
+
export declare const VIDEO_GENERATE_MODEL = "grok-imagine-video-1.5";
|
|
19
|
+
/** Polling endpoint for one generation request. */
|
|
20
|
+
export declare function videoStatusUrl(requestId: string): string;
|
|
21
|
+
/** Default delay between two status polls. */
|
|
22
|
+
export declare const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
23
|
+
/** Default overall deadline for one generation (submit → done). */
|
|
24
|
+
export declare const DEFAULT_MAX_WAIT_MS: number;
|
|
25
|
+
/** Dependencies of the `video_generate` tool. */
|
|
26
|
+
export interface VideoGenerateToolOptions {
|
|
27
|
+
/** Grok session source; a missing session throws the log-in hint. */
|
|
28
|
+
tokens: TokenManager<GrokSession>;
|
|
29
|
+
/** Fetch implementation (injectable for tests). */
|
|
30
|
+
fetchFn?: FetchFn;
|
|
31
|
+
/** Directory override for saved videos (defaults under the harness home). */
|
|
32
|
+
videosDir?: string;
|
|
33
|
+
/** Delay between status polls (injectable for tests). */
|
|
34
|
+
pollIntervalMs?: number;
|
|
35
|
+
/** Overall deadline from submit to completion. */
|
|
36
|
+
maxWaitMs?: number;
|
|
37
|
+
}
|
|
38
|
+
/** The wire request body for one generation call. */
|
|
39
|
+
export interface VideoGenerateRequestBody {
|
|
40
|
+
prompt: string;
|
|
41
|
+
model: string;
|
|
42
|
+
duration?: number;
|
|
43
|
+
aspect_ratio?: string;
|
|
44
|
+
resolution?: string;
|
|
45
|
+
image?: {
|
|
46
|
+
url: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Assemble the request body from tool arguments (hand-checks the non-empty
|
|
51
|
+
* prompt and the duration range the schema DSL cannot express).
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildVideoGenerateBody(args: {
|
|
54
|
+
prompt: string;
|
|
55
|
+
duration?: number;
|
|
56
|
+
aspect_ratio?: '16:9' | '9:16' | '1:1' | '4:3' | '3:4' | '3:2' | '2:3';
|
|
57
|
+
resolution?: '480p' | '720p' | '1080p';
|
|
58
|
+
image_url?: string;
|
|
59
|
+
}): VideoGenerateRequestBody;
|
|
60
|
+
/**
|
|
61
|
+
* Extract the request id from the submit response. Throws when the payload
|
|
62
|
+
* carries none.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseVideoStartResponse(payload: unknown): string;
|
|
65
|
+
/** One decoded poll response. */
|
|
66
|
+
export type VideoStatus = {
|
|
67
|
+
status: 'pending';
|
|
68
|
+
} | {
|
|
69
|
+
status: 'done';
|
|
70
|
+
url: string;
|
|
71
|
+
duration?: number;
|
|
72
|
+
} | {
|
|
73
|
+
status: 'failed' | 'expired';
|
|
74
|
+
detail?: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Decode one poll response. A `done` payload without a video URL and an
|
|
78
|
+
* unrecognized status both throw (the poll loop cannot make progress on
|
|
79
|
+
* either).
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseVideoStatusResponse(payload: unknown): VideoStatus;
|
|
82
|
+
/** Directory the downloaded MP4 files are written to. */
|
|
83
|
+
export declare function videosDirectory(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Build the `video_generate` tool definition.
|
|
86
|
+
* @param options - grok session source, fetch implementation, and video directory.
|
|
87
|
+
* @returns the tool to register on `ctx.tools`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function createVideoGenerateTool(options: VideoGenerateToolOptions): ToolDefinition;
|