dsh-plugin-subscriptions 0.3.0 → 0.4.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 +2 -1
- package/README.zh.md +2 -1
- 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 +552 -43
- package/lib/providers/catalog-store.d.ts +37 -0
- package/lib/providers/catalog-store.js +167 -0
- package/lib/providers/codex.d.ts +18 -3
- package/lib/providers/codex.js +60 -14
- package/lib/providers/common.d.ts +47 -5
- package/lib/providers/common.js +74 -11
- package/lib/providers/grok.d.ts +14 -1
- package/lib/providers/grok.js +25 -8
- 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/grok.js
CHANGED
|
@@ -405,10 +405,15 @@ export async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
|
|
|
405
405
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
406
406
|
export class GrokAdapter extends LlmAdapter {
|
|
407
407
|
options;
|
|
408
|
-
catalog
|
|
408
|
+
catalog;
|
|
409
409
|
constructor(options) {
|
|
410
410
|
super();
|
|
411
411
|
this.options = options;
|
|
412
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
413
|
+
}
|
|
414
|
+
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
415
|
+
async fetchCatalog() {
|
|
416
|
+
return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn);
|
|
412
417
|
}
|
|
413
418
|
providerInfo(provider) {
|
|
414
419
|
return { id: provider, name: 'Grok (Subscription)' };
|
|
@@ -432,7 +437,7 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
432
437
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
433
438
|
// through the refresh-aware path so an expired access token renews here
|
|
434
439
|
// instead of failing discovery into the static fallback.
|
|
435
|
-
const discovered = await this.catalog.get(
|
|
440
|
+
const discovered = await this.catalog.get(() => this.fetchCatalog());
|
|
436
441
|
return discovered.map(model => ({
|
|
437
442
|
provider,
|
|
438
443
|
id: model.id,
|
|
@@ -453,12 +458,24 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
453
458
|
return this.staticModels(provider);
|
|
454
459
|
}
|
|
455
460
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
461
|
+
/**
|
|
462
|
+
* The discovered entry for one model. Resolved through the cache's
|
|
463
|
+
* stale-while-revalidate path: capability metadata must stay stable across
|
|
464
|
+
* a long conversation — a session that selected a reasoning effort calls
|
|
465
|
+
* this on EVERY step, and forgetting the efforts just because the TTL
|
|
466
|
+
* lapsed mid-turn would fail the call with UNSUPPORTED_REASONING_EFFORT
|
|
467
|
+
* before provider I/O.
|
|
468
|
+
*/
|
|
469
|
+
async discovered(model) {
|
|
470
|
+
if (!this.options.discovery)
|
|
471
|
+
return undefined;
|
|
472
|
+
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
473
|
+
return models?.find(entry => entry.id === model);
|
|
474
|
+
}
|
|
475
|
+
async resolveModel(provider, model) {
|
|
476
|
+
const discovered = await this.discovered(model);
|
|
460
477
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
461
|
-
return
|
|
478
|
+
return {
|
|
462
479
|
provider,
|
|
463
480
|
id: model,
|
|
464
481
|
name: discovered?.name ?? configured?.name ?? model,
|
|
@@ -470,7 +487,7 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
470
487
|
// cover expose none, so the harness rejects explicit efforts before
|
|
471
488
|
// provider I/O instead of the API 400ing.
|
|
472
489
|
...discovered?.reasoning === undefined ? {} : { reasoning: discovered.reasoning },
|
|
473
|
-
}
|
|
490
|
+
};
|
|
474
491
|
}
|
|
475
492
|
async *stream(options) {
|
|
476
493
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -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;
|
|
@@ -0,0 +1,255 @@
|
|
|
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 { mkdir, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { basename, join } from 'node:path';
|
|
13
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
14
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
15
|
+
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
16
|
+
/** Endpoint the generation request is posted to. */
|
|
17
|
+
export const VIDEO_GENERATE_URL = 'https://api.x.ai/v1/videos/generations';
|
|
18
|
+
/** The video model the grok subscription endpoint serves. */
|
|
19
|
+
export const VIDEO_GENERATE_MODEL = 'grok-imagine-video-1.5';
|
|
20
|
+
/** Polling endpoint for one generation request. */
|
|
21
|
+
export function videoStatusUrl(requestId) {
|
|
22
|
+
return `https://api.x.ai/v1/videos/${encodeURIComponent(requestId)}`;
|
|
23
|
+
}
|
|
24
|
+
/** Default delay between two status polls. */
|
|
25
|
+
export const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
26
|
+
/** Default overall deadline for one generation (submit → done). */
|
|
27
|
+
export const DEFAULT_MAX_WAIT_MS = 10 * 60_000;
|
|
28
|
+
/** xAI's supported clip length range in seconds. */
|
|
29
|
+
const DURATION_RANGE = { min: 1, max: 15 };
|
|
30
|
+
/**
|
|
31
|
+
* Assemble the request body from tool arguments (hand-checks the non-empty
|
|
32
|
+
* prompt and the duration range the schema DSL cannot express).
|
|
33
|
+
*/
|
|
34
|
+
export function buildVideoGenerateBody(args) {
|
|
35
|
+
const prompt = args.prompt.trim();
|
|
36
|
+
if (prompt.length === 0)
|
|
37
|
+
throw new Error('video_generate: prompt must be a non-empty string');
|
|
38
|
+
if (args.duration !== undefined
|
|
39
|
+
&& (!Number.isInteger(args.duration)
|
|
40
|
+
|| args.duration < DURATION_RANGE.min
|
|
41
|
+
|| args.duration > DURATION_RANGE.max)) {
|
|
42
|
+
throw new Error(`video_generate: duration must be an integer between ${String(DURATION_RANGE.min)} and ${String(DURATION_RANGE.max)} seconds`);
|
|
43
|
+
}
|
|
44
|
+
const imageUrl = args.image_url?.trim();
|
|
45
|
+
return {
|
|
46
|
+
prompt,
|
|
47
|
+
model: VIDEO_GENERATE_MODEL,
|
|
48
|
+
...args.duration === undefined ? {} : { duration: args.duration },
|
|
49
|
+
...args.aspect_ratio === undefined ? {} : { aspect_ratio: args.aspect_ratio },
|
|
50
|
+
...args.resolution === undefined ? {} : { resolution: args.resolution },
|
|
51
|
+
...imageUrl === undefined || imageUrl.length === 0 ? {} : { image: { url: imageUrl } },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract the request id from the submit response. Throws when the payload
|
|
59
|
+
* carries none.
|
|
60
|
+
*/
|
|
61
|
+
export function parseVideoStartResponse(payload) {
|
|
62
|
+
const body = isRecord(payload) ? payload : {};
|
|
63
|
+
if (typeof body.request_id !== 'string' || body.request_id.length === 0) {
|
|
64
|
+
throw new Error('video_generate: the response carried no request_id');
|
|
65
|
+
}
|
|
66
|
+
return body.request_id;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decode one poll response. A `done` payload without a video URL and an
|
|
70
|
+
* unrecognized status both throw (the poll loop cannot make progress on
|
|
71
|
+
* either).
|
|
72
|
+
*/
|
|
73
|
+
export function parseVideoStatusResponse(payload) {
|
|
74
|
+
const body = isRecord(payload) ? payload : {};
|
|
75
|
+
switch (body.status) {
|
|
76
|
+
case 'pending':
|
|
77
|
+
return { status: 'pending' };
|
|
78
|
+
case 'done': {
|
|
79
|
+
const video = isRecord(body.video) ? body.video : {};
|
|
80
|
+
if (typeof video.url !== 'string' || video.url.length === 0) {
|
|
81
|
+
throw new Error('video_generate: the completed response carried no video URL');
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
status: 'done',
|
|
85
|
+
url: video.url,
|
|
86
|
+
...typeof video.duration === 'number' ? { duration: video.duration } : {},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case 'failed':
|
|
90
|
+
case 'expired': {
|
|
91
|
+
const error = isRecord(body.error) ? body.error : {};
|
|
92
|
+
const detail = typeof error.message === 'string' && error.message.length > 0
|
|
93
|
+
? error.message
|
|
94
|
+
: typeof body.error === 'string' && body.error.length > 0 ? body.error : undefined;
|
|
95
|
+
return { status: body.status, ...detail === undefined ? {} : { detail } };
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`video_generate: unexpected status ${JSON.stringify(body.status)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Directory the downloaded MP4 files are written to. */
|
|
102
|
+
export function videosDirectory() {
|
|
103
|
+
return dshHomePath('plugins', 'subscriptions', 'videos');
|
|
104
|
+
}
|
|
105
|
+
/** Timestamped, collision-safe file name for one generated video. */
|
|
106
|
+
function videoFileName() {
|
|
107
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
108
|
+
return `video-${stamp}-${Math.random().toString(36).slice(2, 8)}.mp4`;
|
|
109
|
+
}
|
|
110
|
+
/** Bound a call-card title's prompt. */
|
|
111
|
+
function truncate(text, max = 60) {
|
|
112
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
113
|
+
}
|
|
114
|
+
/** Abort-aware sleep between two polls. */
|
|
115
|
+
function sleep(ms, signal) {
|
|
116
|
+
if (ms <= 0)
|
|
117
|
+
return Promise.resolve();
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const onAbort = () => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('video_generate: aborted'));
|
|
122
|
+
};
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
signal.removeEventListener('abort', onAbort);
|
|
125
|
+
resolve();
|
|
126
|
+
}, ms);
|
|
127
|
+
if (signal.aborted) {
|
|
128
|
+
onAbort();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build the `video_generate` tool definition.
|
|
136
|
+
* @param options - grok session source, fetch implementation, and video directory.
|
|
137
|
+
* @returns the tool to register on `ctx.tools`.
|
|
138
|
+
*/
|
|
139
|
+
export function createVideoGenerateTool(options) {
|
|
140
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
141
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
142
|
+
return defineTool({
|
|
143
|
+
name: 'video_generate',
|
|
144
|
+
description: `Generate a short video (1-15 seconds) with the grok subscription (${VIDEO_GENERATE_MODEL}) `
|
|
145
|
+
+ 'and save it as an MP4 file. Generation is asynchronous and may take a minute or more; '
|
|
146
|
+
+ 'the tool waits for completion and returns the saved file path. '
|
|
147
|
+
+ 'Optionally animate a still image by passing image_url (image-to-video).',
|
|
148
|
+
parameters: {
|
|
149
|
+
prompt: { type: 'string', required: true, description: 'What the video should show.' },
|
|
150
|
+
duration: {
|
|
151
|
+
type: 'integer',
|
|
152
|
+
description: 'Clip length in seconds (1-15); omit for the provider default.',
|
|
153
|
+
},
|
|
154
|
+
aspect_ratio: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
enum: ['16:9', '9:16', '1:1', '4:3', '3:4', '3:2', '2:3'],
|
|
157
|
+
description: 'Output aspect ratio; omit for the provider default (16:9).',
|
|
158
|
+
},
|
|
159
|
+
resolution: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
enum: ['480p', '720p', '1080p'],
|
|
162
|
+
description: 'Output resolution; omit for the provider default (480p). Higher is slower.',
|
|
163
|
+
},
|
|
164
|
+
image_url: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Optional public URL or base64 data URL of a JPEG/PNG/WebP image to animate '
|
|
167
|
+
+ '(image-to-video); the image becomes the starting frame.',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
output: {
|
|
171
|
+
schema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
path: { type: 'string', required: true },
|
|
175
|
+
url: { type: 'string', required: true },
|
|
176
|
+
duration: { type: 'number' },
|
|
177
|
+
},
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
},
|
|
180
|
+
render: (_args, value) => [{
|
|
181
|
+
type: 'text',
|
|
182
|
+
text: `Saved video to ${value.path}`
|
|
183
|
+
+ (value.duration === undefined ? '' : ` (${String(value.duration)}s)`)
|
|
184
|
+
+ `\nTemporary provider URL (expires soon): ${value.url}`,
|
|
185
|
+
}],
|
|
186
|
+
// The client toolview fetches the bytes by bare file name through the
|
|
187
|
+
// `/subscriptions-auth` `video` endpoint; meta hands it that name.
|
|
188
|
+
presentationMeta: (_args, value) => ({
|
|
189
|
+
fileName: basename(value.path),
|
|
190
|
+
...value.duration === undefined ? {} : { duration: value.duration },
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
presentCall: args => ({
|
|
194
|
+
card: 'generic',
|
|
195
|
+
title: `video_generate: ${truncate(args.prompt)}`,
|
|
196
|
+
}),
|
|
197
|
+
async execute(args, exec) {
|
|
198
|
+
const body = buildVideoGenerateBody(args);
|
|
199
|
+
const session = await options.tokens.session();
|
|
200
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
201
|
+
const headers = {
|
|
202
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
203
|
+
'accept': 'application/json',
|
|
204
|
+
};
|
|
205
|
+
const submit = await fetchFn(VIDEO_GENERATE_URL, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { ...headers, 'content-type': 'application/json' },
|
|
208
|
+
body: JSON.stringify(body),
|
|
209
|
+
signal: exec.signal,
|
|
210
|
+
});
|
|
211
|
+
if (!submit.ok)
|
|
212
|
+
throw await httpLlmError(submit, 'video_generate');
|
|
213
|
+
const requestId = parseVideoStartResponse(await submit.json());
|
|
214
|
+
const deadline = Date.now() + maxWaitMs;
|
|
215
|
+
let done;
|
|
216
|
+
for (;;) {
|
|
217
|
+
await sleep(pollIntervalMs, exec.signal);
|
|
218
|
+
const poll = await fetchFn(videoStatusUrl(requestId), {
|
|
219
|
+
method: 'GET',
|
|
220
|
+
headers,
|
|
221
|
+
signal: exec.signal,
|
|
222
|
+
});
|
|
223
|
+
if (!poll.ok)
|
|
224
|
+
throw await httpLlmError(poll, 'video_generate');
|
|
225
|
+
const status = parseVideoStatusResponse(await poll.json());
|
|
226
|
+
if (status.status === 'done') {
|
|
227
|
+
done = status;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
if (status.status === 'failed' || status.status === 'expired') {
|
|
231
|
+
throw new Error(`video_generate: generation ${status.status} (request ${requestId})`
|
|
232
|
+
+ (status.detail === undefined ? '' : `: ${status.detail}`));
|
|
233
|
+
}
|
|
234
|
+
if (Date.now() >= deadline) {
|
|
235
|
+
throw new Error(`video_generate: timed out after ${String(maxWaitMs)}ms waiting for request ${requestId}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// The MP4 lives on a separate signed host: no auth header on purpose.
|
|
239
|
+
const download = await fetchFn(done.url, { method: 'GET', signal: exec.signal });
|
|
240
|
+
if (!download.ok)
|
|
241
|
+
throw await httpLlmError(download, 'video_generate download');
|
|
242
|
+
const data = Buffer.from(await download.arrayBuffer());
|
|
243
|
+
const directory = options.videosDir ?? videosDirectory();
|
|
244
|
+
await mkdir(directory, { recursive: true });
|
|
245
|
+
const path = join(directory, videoFileName());
|
|
246
|
+
await writeFile(path, data);
|
|
247
|
+
const value = {
|
|
248
|
+
path,
|
|
249
|
+
url: done.url,
|
|
250
|
+
...done.duration === undefined ? {} : { duration: done.duration },
|
|
251
|
+
};
|
|
252
|
+
return value;
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscriptions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|
|
@@ -48,6 +48,12 @@
|
|
|
48
48
|
]
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc && tsdown",
|
|
53
|
+
"test": "tsc -p tsconfig.test.json && node --test lib-test/test/",
|
|
54
|
+
"prepare": "tsdown -c tsdown.prepare.config.ts",
|
|
55
|
+
"prepublishOnly": "pnpm build && pnpm test"
|
|
56
|
+
},
|
|
51
57
|
"peerDependencies": {
|
|
52
58
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
59
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.5",
|
|
@@ -75,9 +81,5 @@
|
|
|
75
81
|
"react": "^18.2.0",
|
|
76
82
|
"tsdown": "^0.15.0",
|
|
77
83
|
"typescript": "^5.8.0"
|
|
78
|
-
},
|
|
79
|
-
"scripts": {
|
|
80
|
-
"build": "tsc && tsdown",
|
|
81
|
-
"test": "tsc -p tsconfig.test.json && node --test lib-test/test/"
|
|
82
84
|
}
|
|
83
|
-
}
|
|
85
|
+
}
|