dsh-plugin-subscriptions 0.4.0 → 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 +9 -1
- package/README.zh.md +9 -1
- package/lib/index.js +103 -25
- package/lib/tools/image-generate.d.ts +55 -19
- package/lib/tools/image-generate.js +130 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,14 @@ The `image_generate` tool renders its result inline in the conversation:
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
+
Its `provider` parameter picks the image backend — the same prompt through GPT (`gpt-image-2`, top) and Grok (`grok-imagine-image-2.0`, bottom):
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
The `video_generate` tool plays the generated clip inline:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
25
33
|
## Providers
|
|
26
34
|
|
|
27
35
|
| Route | Subscription | Models |
|
|
@@ -37,7 +45,7 @@ Logged-in cards also show **subscription usage** — per rate-limit window (5-ho
|
|
|
37
45
|
Also included, registered when the matching provider is enabled:
|
|
38
46
|
|
|
39
47
|
- **`x_search`** tool (Grok) — xAI's hosted X search, returning `{ answer, citations }`.
|
|
40
|
-
- **`image_generate`** tool (ChatGPT) — `gpt-image-2` via the Codex backend;
|
|
48
|
+
- **`image_generate`** tool (ChatGPT or Grok) — `gpt-image-2` via the Codex backend, or `grok-imagine-image-2.0` via `api.x.ai/v1/images/generations`. The `provider` argument picks the preferred provider (`gpt`, the default, or `grok`); when the preferred one is logged out the other serves as fallback. Images are saved under `~/.dsh/plugins/subscriptions/images/` and the paths returned. The `size`/`quality` arguments map onto Grok's `aspect_ratio`/`quality` on the Grok path.
|
|
41
49
|
- **`video_generate`** tool (Grok) — `grok-imagine-video-1.5` via `api.x.ai/v1/videos` (async submit + poll); MP4s are saved under `~/.dsh/plugins/subscriptions/videos/`, the path returned, and the clip plays inline in the conversation. Supports duration (1–15 s), aspect ratio, resolution, and image-to-video via `image_url`.
|
|
42
50
|
|
|
43
51
|
## Install
|
package/README.zh.md
CHANGED
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
+
`provider` 参数可选择生图后端——同一条提示词分别走 GPT(`gpt-image-2`,上)和 Grok(`grok-imagine-image-2.0`,下):
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
`video_generate` 工具生成的视频直接内联播放:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
25
33
|
## Provider 一览
|
|
26
34
|
|
|
27
35
|
| 路由 | 订阅 | 模型 |
|
|
@@ -37,7 +45,7 @@
|
|
|
37
45
|
随 provider 启用自动注册的工具:
|
|
38
46
|
|
|
39
47
|
- **`x_search`**(Grok)—— xAI 托管的 X 搜索,返回 `{ answer, citations }`。
|
|
40
|
-
- **`image_generate`**(ChatGPT)—— 经 Codex 后端调用 `gpt-image-2
|
|
48
|
+
- **`image_generate`**(ChatGPT 或 Grok)—— 经 Codex 后端调用 `gpt-image-2`,或经 `api.x.ai/v1/images/generations` 调用 `grok-imagine-image-2.0`。`provider` 参数指定首选提供方(`gpt` 为默认值,可选 `grok`);首选方未登录时自动回退到另一方。图片保存到 `~/.dsh/plugins/subscriptions/images/` 并返回路径。Grok 路径上 `size`/`quality` 参数会映射为 Grok 的 `aspect_ratio`/`quality`。
|
|
41
49
|
- **`video_generate`**(Grok)—— 经 `api.x.ai/v1/videos` 调用 `grok-imagine-video-1.5`(异步提交 + 轮询);MP4 保存到 `~/.dsh/plugins/subscriptions/videos/` 并返回路径,视频直接在对话里内联播放。支持时长(1–15 秒)、宽高比、分辨率,以及通过 `image_url` 做图生视频。
|
|
42
50
|
|
|
43
51
|
## 安装
|
package/lib/index.js
CHANGED
|
@@ -3078,13 +3078,17 @@ function createXSearchTool(options) {
|
|
|
3078
3078
|
|
|
3079
3079
|
//#endregion
|
|
3080
3080
|
//#region src/tools/image-generate.ts
|
|
3081
|
-
/** Endpoint the generation request is posted to. */
|
|
3081
|
+
/** Endpoint the codex generation request is posted to. */
|
|
3082
3082
|
const IMAGE_GENERATE_URL = "https://chatgpt.com/backend-api/codex/images/generations";
|
|
3083
3083
|
/** The image model the codex subscription endpoint serves. */
|
|
3084
3084
|
const IMAGE_GENERATE_MODEL = "gpt-image-2";
|
|
3085
|
+
/** Endpoint the grok generation request is posted to. */
|
|
3086
|
+
const GROK_IMAGE_GENERATE_URL = "https://api.x.ai/v1/images/generations";
|
|
3087
|
+
/** The image model the grok subscription endpoint serves. */
|
|
3088
|
+
const GROK_IMAGE_GENERATE_MODEL = "grok-imagine-image-2.0";
|
|
3085
3089
|
/**
|
|
3086
|
-
* Assemble the request body from tool arguments (hand-checks the
|
|
3087
|
-
* prompt the schema DSL cannot express).
|
|
3090
|
+
* Assemble the codex request body from tool arguments (hand-checks the
|
|
3091
|
+
* non-empty prompt the schema DSL cannot express).
|
|
3088
3092
|
*/
|
|
3089
3093
|
function buildImageGenerateBody(args) {
|
|
3090
3094
|
const prompt = args.prompt.trim();
|
|
@@ -3096,6 +3100,30 @@ function buildImageGenerateBody(args) {
|
|
|
3096
3100
|
...args.quality === void 0 ? {} : { quality: args.quality }
|
|
3097
3101
|
};
|
|
3098
3102
|
}
|
|
3103
|
+
/** The codex `size` values mapped onto grok aspect ratios. */
|
|
3104
|
+
const GROK_ASPECT_RATIOS = {
|
|
3105
|
+
"1024x1024": "1:1",
|
|
3106
|
+
"1024x1536": "2:3",
|
|
3107
|
+
"1536x1024": "3:2",
|
|
3108
|
+
"auto": "auto"
|
|
3109
|
+
};
|
|
3110
|
+
/**
|
|
3111
|
+
* Assemble the grok request body from the same tool arguments: `size` maps
|
|
3112
|
+
* onto the nearest `aspect_ratio`, and `quality` folds into grok's low/medium
|
|
3113
|
+
* pair (`high` → `medium`, `auto` → provider default).
|
|
3114
|
+
*/
|
|
3115
|
+
function buildGrokImageGenerateBody(args) {
|
|
3116
|
+
const prompt = args.prompt.trim();
|
|
3117
|
+
if (prompt.length === 0) throw new Error("image_generate: prompt must be a non-empty string");
|
|
3118
|
+
const quality = args.quality === "low" ? "low" : args.quality === "medium" || args.quality === "high" ? "medium" : void 0;
|
|
3119
|
+
return {
|
|
3120
|
+
prompt,
|
|
3121
|
+
model: GROK_IMAGE_GENERATE_MODEL,
|
|
3122
|
+
response_format: "b64_json",
|
|
3123
|
+
...args.size === void 0 ? {} : { aspect_ratio: GROK_ASPECT_RATIOS[args.size] },
|
|
3124
|
+
...quality === void 0 ? {} : { quality }
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3099
3127
|
/**
|
|
3100
3128
|
* Parse the generations response into decodable images. Throws when the
|
|
3101
3129
|
* payload carries no usable `b64_json` entries.
|
|
@@ -3116,13 +3144,29 @@ function parseImageGenerateResponse(payload) {
|
|
|
3116
3144
|
if (images.length === 0) throw new Error("image_generate: the response carried no image data");
|
|
3117
3145
|
return images;
|
|
3118
3146
|
}
|
|
3119
|
-
/** Directory the generated
|
|
3147
|
+
/** Directory the generated image files are written to. */
|
|
3120
3148
|
function imagesDirectory() {
|
|
3121
3149
|
return dshHomePath("plugins", "subscriptions", "images");
|
|
3122
3150
|
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Sniff a generated image's media type from its magic bytes (codex serves
|
|
3153
|
+
* PNG; grok's format is undocumented, so trust the bytes). Unrecognized data
|
|
3154
|
+
* defaults to PNG, matching the historical behavior.
|
|
3155
|
+
*/
|
|
3156
|
+
function sniffImageMediaType(data) {
|
|
3157
|
+
if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
3158
|
+
if (data.length >= 12 && data.toString("latin1", 0, 4) === "RIFF" && data.toString("latin1", 8, 12) === "WEBP") return "image/webp";
|
|
3159
|
+
return "image/png";
|
|
3160
|
+
}
|
|
3161
|
+
/** File extension for one sniffed media type. */
|
|
3162
|
+
const MEDIA_TYPE_EXTENSIONS = {
|
|
3163
|
+
"image/png": "png",
|
|
3164
|
+
"image/jpeg": "jpg",
|
|
3165
|
+
"image/webp": "webp"
|
|
3166
|
+
};
|
|
3123
3167
|
/** Timestamped, collision-safe file name for one generated image. */
|
|
3124
|
-
function imageFileName(index) {
|
|
3125
|
-
return `image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${index}
|
|
3168
|
+
function imageFileName(index, mediaType) {
|
|
3169
|
+
return `image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${index}.${MEDIA_TYPE_EXTENSIONS[mediaType]}`;
|
|
3126
3170
|
}
|
|
3127
3171
|
/** Bound a call-card title's prompt. */
|
|
3128
3172
|
function truncate$1(text, max = 60) {
|
|
@@ -3180,7 +3224,7 @@ function imageGenerateText(value) {
|
|
|
3180
3224
|
function createImageGenerateTool(options) {
|
|
3181
3225
|
return defineTool({
|
|
3182
3226
|
name: "image_generate",
|
|
3183
|
-
description: "Generate an image with the ChatGPT subscription (gpt-image-2) and save it as
|
|
3227
|
+
description: "Generate an image with the ChatGPT subscription (gpt-image-2) or the Grok subscription (grok-imagine-image-2.0) and save it as an image file. The `provider` parameter picks the preferred provider (default gpt); when the preferred one is logged out the other serves as fallback. Returns the saved file paths; on image-capable models the image itself is attached.",
|
|
3184
3228
|
parameters: {
|
|
3185
3229
|
prompt: {
|
|
3186
3230
|
type: "string",
|
|
@@ -3206,6 +3250,11 @@ function createImageGenerateTool(options) {
|
|
|
3206
3250
|
"auto"
|
|
3207
3251
|
],
|
|
3208
3252
|
description: "Rendering quality; omit for the provider default."
|
|
3253
|
+
},
|
|
3254
|
+
provider: {
|
|
3255
|
+
type: "string",
|
|
3256
|
+
enum: ["gpt", "grok"],
|
|
3257
|
+
description: "Preferred provider (default gpt); the other one serves as fallback when the preferred is logged out."
|
|
3209
3258
|
}
|
|
3210
3259
|
},
|
|
3211
3260
|
output: {
|
|
@@ -3268,29 +3317,57 @@ function createImageGenerateTool(options) {
|
|
|
3268
3317
|
content: result.content.filter((block) => block.type === "text")
|
|
3269
3318
|
}),
|
|
3270
3319
|
async execute(args, exec) {
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
3273
|
-
const
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3320
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
3321
|
+
const preferGrok = args.provider === "grok";
|
|
3322
|
+
const codexReady = options.codexTokens !== void 0 && await options.codexTokens.hasSession();
|
|
3323
|
+
const grokReady = options.grokTokens !== void 0 && await options.grokTokens.hasSession();
|
|
3324
|
+
const useGrok = preferGrok ? grokReady : grokReady && !codexReady;
|
|
3325
|
+
const useCodex = !useGrok && codexReady;
|
|
3326
|
+
let response;
|
|
3327
|
+
if (useCodex && options.codexTokens !== void 0) {
|
|
3328
|
+
const session = await options.codexTokens.session();
|
|
3329
|
+
response = await fetchFn(IMAGE_GENERATE_URL, {
|
|
3330
|
+
method: "POST",
|
|
3331
|
+
headers: {
|
|
3332
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
3333
|
+
"chatgpt-account-id": session.accountId,
|
|
3334
|
+
"originator": "codex_cli_rs",
|
|
3335
|
+
"content-type": "application/json",
|
|
3336
|
+
"accept": "application/json"
|
|
3337
|
+
},
|
|
3338
|
+
body: JSON.stringify(buildImageGenerateBody(args)),
|
|
3339
|
+
signal: exec.signal
|
|
3340
|
+
});
|
|
3341
|
+
} else if (useGrok && options.grokTokens !== void 0) {
|
|
3342
|
+
const session = await options.grokTokens.session();
|
|
3343
|
+
response = await fetchFn(GROK_IMAGE_GENERATE_URL, {
|
|
3344
|
+
method: "POST",
|
|
3345
|
+
headers: {
|
|
3346
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
3347
|
+
"content-type": "application/json",
|
|
3348
|
+
"accept": "application/json"
|
|
3349
|
+
},
|
|
3350
|
+
body: JSON.stringify(buildGrokImageGenerateBody(args)),
|
|
3351
|
+
signal: exec.signal
|
|
3352
|
+
});
|
|
3353
|
+
} else {
|
|
3354
|
+
const manager = preferGrok ? options.grokTokens ?? options.codexTokens : options.codexTokens ?? options.grokTokens;
|
|
3355
|
+
if (manager === void 0) throw new Error("image_generate: no image provider is configured");
|
|
3356
|
+
await manager.session();
|
|
3357
|
+
throw new Error("image_generate: no image provider is logged in");
|
|
3358
|
+
}
|
|
3285
3359
|
if (!response.ok) throw await httpLlmError(response, "image_generate");
|
|
3286
3360
|
const images = parseImageGenerateResponse(await response.json());
|
|
3287
3361
|
const directory = options.imagesDir ?? imagesDirectory();
|
|
3288
3362
|
await mkdir(directory, { recursive: true });
|
|
3289
3363
|
const paths = [];
|
|
3364
|
+
const mediaTypes = [];
|
|
3290
3365
|
for (const [index, image] of images.entries()) {
|
|
3291
|
-
const
|
|
3366
|
+
const mediaType = sniffImageMediaType(image.data);
|
|
3367
|
+
const path = join(directory, imageFileName(index, mediaType));
|
|
3292
3368
|
await writeFile(path, image.data);
|
|
3293
3369
|
paths.push(path);
|
|
3370
|
+
mediaTypes.push(mediaType);
|
|
3294
3371
|
}
|
|
3295
3372
|
const attachments = options.resolveAttachments?.();
|
|
3296
3373
|
const imageCapable = attachments !== void 0 && await routeDeclaresImageInput(options.resolveLlm, exec);
|
|
@@ -3298,7 +3375,7 @@ function createImageGenerateTool(options) {
|
|
|
3298
3375
|
if (attachments !== void 0 && imageCapable) for (const [index, image] of images.entries()) {
|
|
3299
3376
|
const ref = await attachments.saveImage({
|
|
3300
3377
|
data: image.data,
|
|
3301
|
-
mediaType:
|
|
3378
|
+
mediaType: mediaTypes[index],
|
|
3302
3379
|
name: basename(paths[index])
|
|
3303
3380
|
});
|
|
3304
3381
|
refs.push({
|
|
@@ -3867,8 +3944,9 @@ function apply(ctx, config) {
|
|
|
3867
3944
|
toolsCtx.tools.register(createXSearchTool({ tokens: grokTokens }));
|
|
3868
3945
|
toolsCtx.tools.register(createVideoGenerateTool({ tokens: grokTokens }));
|
|
3869
3946
|
}
|
|
3870
|
-
if (codexTokens !== void 0) toolsCtx.tools.register(createImageGenerateTool({
|
|
3871
|
-
|
|
3947
|
+
if (codexTokens !== void 0 || grokTokens !== void 0) toolsCtx.tools.register(createImageGenerateTool({
|
|
3948
|
+
...codexTokens === void 0 ? {} : { codexTokens },
|
|
3949
|
+
...grokTokens === void 0 ? {} : { grokTokens },
|
|
3872
3950
|
resolveAttachments,
|
|
3873
3951
|
resolveLlm: () => ctx.get("llm")
|
|
3874
3952
|
}));
|
|
@@ -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({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscriptions",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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": {
|