dsh-coding-subscription-oauth 0.6.3 → 0.6.4

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.
Files changed (116) hide show
  1. package/CHANGELOG.md +258 -250
  2. package/CONTRIBUTING.md +129 -129
  3. package/INSTALL.md +256 -255
  4. package/LICENSE +19 -19
  5. package/NOTICE +11 -11
  6. package/README.de.md +303 -303
  7. package/README.es.md +304 -304
  8. package/README.fr.md +304 -304
  9. package/README.ja.md +304 -304
  10. package/README.ko.md +304 -304
  11. package/README.md +320 -320
  12. package/README.pt-BR.md +304 -304
  13. package/README.ru.md +304 -304
  14. package/README.zh-CN.md +318 -318
  15. package/compatibility/dsh-bom.json +30 -30
  16. package/cordis.patch.yml +13 -13
  17. package/docs/00-project-rules.md +212 -212
  18. package/docs/02-architecture.md +138 -138
  19. package/docs/02-architecture.zh-CN.md +138 -138
  20. package/lib/bin.js +1333 -569
  21. package/lib/bin.js.map +4 -4
  22. package/lib/client.js +1 -1
  23. package/lib/client.js.map +2 -2
  24. package/lib/index.js +689 -25492
  25. package/lib/index.js.map +4 -4
  26. package/lib/invariant.js.map +1 -1
  27. package/media/en/settings_accounts.png +0 -0
  28. package/media/en/settings_capabilities.png +0 -0
  29. package/media/en/settings_gateway.png +0 -0
  30. package/media/settings_accounts.png +0 -0
  31. package/media/settings_capabilities.png +0 -0
  32. package/media/settings_gateway.png +0 -0
  33. package/media/settings_overview.png +0 -0
  34. package/media/zh-CN/settings_accounts.png +0 -0
  35. package/media/zh-CN/settings_capabilities.png +0 -0
  36. package/media/zh-CN/settings_gateway.png +0 -0
  37. package/package.json +223 -223
  38. package/patches/dsh-agy@0.1.2.patch +25 -25
  39. package/scripts/release.mjs +186 -186
  40. package/scripts/smoke-deployed-routes.mjs +146 -146
  41. package/scripts/verify-deployed-catalog.mjs +87 -87
  42. package/src/adapter.ts +348 -348
  43. package/src/alias-adapter.ts +147 -147
  44. package/src/auth-routes.ts +921 -921
  45. package/src/auth.ts +67 -67
  46. package/src/bin.ts +350 -350
  47. package/src/capability-routes.ts +279 -279
  48. package/src/capability-runtime.ts +314 -314
  49. package/src/capability-settings.ts +671 -671
  50. package/src/capability-tools.ts +685 -685
  51. package/src/catalog.ts +271 -271
  52. package/src/client/GrokBuildSettings.tsx +771 -771
  53. package/src/client/api.ts +88 -88
  54. package/src/client/components/AboutTab.tsx +30 -30
  55. package/src/client/components/AccountsTab.tsx +241 -241
  56. package/src/client/components/Badge.tsx +33 -33
  57. package/src/client/components/CapabilitiesTab.tsx +265 -265
  58. package/src/client/components/CliPullPreview.tsx +116 -116
  59. package/src/client/components/CopyButton.tsx +57 -57
  60. package/src/client/components/GatewayTab.tsx +469 -469
  61. package/src/client/components/NoticeBanner.tsx +46 -46
  62. package/src/client/components/ProgressBar.tsx +53 -53
  63. package/src/client/components/ProviderCard.tsx +606 -606
  64. package/src/client/components/SettingsTabs.tsx +75 -75
  65. package/src/client/components/ToggleSwitch.tsx +71 -71
  66. package/src/client/constants.ts +230 -230
  67. package/src/client/display.ts +61 -61
  68. package/src/client/dshClientAdapter.ts +127 -127
  69. package/src/client/gatewaySnippets.ts +37 -37
  70. package/src/client/index.tsx +156 -156
  71. package/src/client/locales.ts +540 -540
  72. package/src/client/microStyles.ts +52 -52
  73. package/src/client/parsers.ts +398 -398
  74. package/src/client/styles.ts +325 -325
  75. package/src/client/types.ts +199 -199
  76. package/src/codex-http.ts +447 -447
  77. package/src/codex-images.ts +503 -503
  78. package/src/codex-model-capabilities.ts +320 -320
  79. package/src/codex-search.ts +245 -245
  80. package/src/codex-usage.ts +263 -263
  81. package/src/compatibility.ts +55 -55
  82. package/src/dsh-host-adapter.ts +173 -173
  83. package/src/gateway-anthropic-messages.ts +84 -84
  84. package/src/gateway-auth.ts +102 -102
  85. package/src/gateway-backend.ts +274 -274
  86. package/src/gateway-body.ts +49 -49
  87. package/src/gateway-config.ts +76 -76
  88. package/src/gateway-http.ts +104 -104
  89. package/src/gateway-openai-chat.ts +124 -124
  90. package/src/gateway-openai-responses.ts +53 -53
  91. package/src/gateway-parse.ts +224 -224
  92. package/src/gateway-protocol.ts +52 -52
  93. package/src/gateway-routes.ts +158 -158
  94. package/src/gateway.ts +258 -258
  95. package/src/grok-errors.ts +24 -24
  96. package/src/grok-imagine.ts +1627 -1627
  97. package/src/grok-import.ts +151 -151
  98. package/src/http-json.ts +82 -82
  99. package/src/ids.ts +59 -59
  100. package/src/imagine-routes.ts +463 -463
  101. package/src/index.ts +735 -735
  102. package/src/invariant.ts +17 -17
  103. package/src/kimi-errors.ts +26 -26
  104. package/src/media-store.ts +927 -927
  105. package/src/oauth-import-routes.ts +324 -324
  106. package/src/oauth-providers.ts +152 -152
  107. package/src/oauth-session.ts +183 -183
  108. package/src/oauth-sources.ts +1104 -1104
  109. package/src/oauth.ts +620 -620
  110. package/src/provider.ts +128 -128
  111. package/src/proxy.ts +11 -11
  112. package/src/redact.ts +72 -72
  113. package/src/session.ts +218 -218
  114. package/src/store.ts +217 -217
  115. package/src/web-origin.ts +296 -296
  116. package/src/web-routes.ts +38 -38
@@ -1,685 +1,685 @@
1
- /**
2
- * Optional Codex / Grok Imagine tool definitions. The factory only returns
3
- * public `ToolDefinition` objects — it never registers on `ctx.tools`.
4
- * Feature flags and `imageCount` are re-read from `current()` at execute time.
5
- * @module dsh-coding-subscription-oauth/capability-tools
6
- */
7
-
8
- import type { ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
9
- import { LlmError } from "@deepseek-ai/dsh-llm";
10
- import type { ToolDefinition, ToolRunContext } from "@deepseek-ai/dsh-tools";
11
- import { CAPABILITY_SETTINGS_BOUNDS, type CapabilitySettings } from "./capability-settings.ts";
12
- import type { CodexAuthSession } from "./codex-http.ts";
13
- import {
14
- CODEX_IMAGE_BACKGROUNDS,
15
- CODEX_IMAGE_MODEL,
16
- CODEX_IMAGE_PROMPT_MAX_LENGTH,
17
- CODEX_IMAGE_QUALITIES,
18
- CODEX_IMAGE_SIZES,
19
- type CodexImageAttachmentStore,
20
- type CodexImageBackground,
21
- type CodexImageController,
22
- type CodexImageQuality,
23
- type CodexImageResult,
24
- type CodexImageRoute,
25
- type CodexImageRoutePolicy,
26
- type CodexImageSessionContext,
27
- type CodexImageSize,
28
- createCodexImageController,
29
- } from "./codex-images.ts";
30
- import {
31
- type GenerateImagineImageInput,
32
- GROK_IMAGINE_IMAGE_MODEL,
33
- GROK_IMAGINE_IMAGE_TOOL,
34
- GROK_IMAGINE_VIDEO_MODEL,
35
- GROK_IMAGINE_VIDEO_STATUS_TOOL,
36
- GROK_IMAGINE_VIDEO_TOOL,
37
- type GrokImagineClient,
38
- IMAGINE_IMAGE_ASPECT_RATIOS,
39
- IMAGINE_IMAGE_RESOLUTIONS,
40
- IMAGINE_PROMPT_MAX_LENGTH,
41
- IMAGINE_VIDEO_ASPECT_RATIOS,
42
- IMAGINE_VIDEO_MAX_DURATION_SECONDS,
43
- IMAGINE_VIDEO_MIN_DURATION_SECONDS,
44
- IMAGINE_VIDEO_RESOLUTIONS,
45
- type ImagineImageAspectRatio,
46
- type ImagineImageResolution,
47
- type ImagineImageResult,
48
- type ImagineVideoAspectRatio,
49
- type ImagineVideoResolution,
50
- type ImagineVideoStartResult,
51
- type ImagineVideoStatusResult,
52
- imagineImagePath,
53
- parseVideoRequestId,
54
- type StartImagineVideoInput,
55
- } from "./grok-imagine.ts";
56
- import { imagineMediaPath, type MediaArtifactMeta } from "./media-store.ts";
57
-
58
- export const CODEX_IMAGE_GENERATE_TOOL = "codex_image_generate";
59
- export const CODEX_IMAGE_EDIT_TOOL = "codex_image_edit";
60
-
61
- export {
62
- GROK_IMAGINE_IMAGE_TOOL,
63
- GROK_IMAGINE_VIDEO_STATUS_TOOL,
64
- GROK_IMAGINE_VIDEO_TOOL,
65
- } from "./grok-imagine.ts";
66
-
67
- const IMAGE_COUNT_VALUES = [1, 2, 3, 4] as const;
68
- const VIDEO_DURATION_VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as const;
69
- const IMAGINE_IMAGE_PREFIX = "/plugins/dsh-grok-build/imagine/images/";
70
- const IMAGINE_MEDIA_PREFIX = "/plugins/dsh-grok-build/imagine/media/";
71
-
72
- /** Shared client surface; production passes one `GrokImagineClient` so video status can see started jobs. */
73
- export type CapabilityImagineClient = Pick<GrokImagineClient, "generateImage" | "startVideo" | "videoStatus">;
74
-
75
- /** Per-exec Codex controller factory. Tests inject a fake; production binds auth + attachments. */
76
- export type CreateCodexImageController = (
77
- session: CodexImageSessionContext,
78
- routePolicy: CodexImageRoutePolicy,
79
- ) => CodexImageController;
80
-
81
- /**
82
- * Route gate for the Codex image tools. `codexImagesAnyModel` relaxes the
83
- * Codex-route requirement; otherwise the gate stays `codex-capable`.
84
- */
85
- export function codexImageRoutePolicy(settings: CapabilitySettings): CodexImageRoutePolicy {
86
- return settings.codexImagesAnyModel ? "any" : "codex-capable";
87
- }
88
-
89
- /** Resolve authoritative host model metadata for the calling route. */
90
- export type ResolveCodexImageRoute = (exec: ToolRunContext) => Promise<CodexImageRoute | undefined>;
91
-
92
- /** Exact provider/model identity from the live request header, then agent options. */
93
- export function callingRouteIdentity(exec: ToolRunContext): { provider: string; model: string } | undefined {
94
- const session = exec.agent?.session;
95
- const header =
96
- session !== undefined && typeof session.requestHeader === "function" ? session.requestHeader()?.config : undefined;
97
- const provider = header?.provider ?? exec.agent?.options.provider;
98
- const model = header?.model ?? exec.agent?.options.model;
99
- if (typeof provider !== "string" || provider.length === 0) return undefined;
100
- if (typeof model !== "string" || model.length === 0) return undefined;
101
- return { provider, model };
102
- }
103
-
104
- /**
105
- * Copy host-resolved modalities onto the calling identity. Never invents
106
- * `inputModalities`; lookup failures keep the identity and omit the field so
107
- * the image-capability gate fails closed.
108
- */
109
- export async function resolveCodexImageRouteFromLlm(
110
- exec: ToolRunContext,
111
- resolveModelInfo: (
112
- provider: string,
113
- model: string,
114
- signal?: AbortSignal,
115
- ) => Promise<{ inputModalities?: readonly string[] }>,
116
- ): Promise<CodexImageRoute | undefined> {
117
- const identity = callingRouteIdentity(exec);
118
- if (identity === undefined) return undefined;
119
- try {
120
- const info = await resolveModelInfo(identity.provider, identity.model, exec.signal);
121
- const modalities = info.inputModalities;
122
- return {
123
- ...identity,
124
- ...(Array.isArray(modalities) ? { inputModalities: [...modalities] } : {}),
125
- };
126
- } catch {
127
- return identity;
128
- }
129
- }
130
-
131
- export interface CapabilityToolsOptions {
132
- /** Live capability section. Re-read on every execute so a disable takes effect immediately. */
133
- current(): CapabilitySettings;
134
- readonly auth: CodexAuthSession;
135
- readonly attachments: CodexImageAttachmentStore;
136
- readonly imagine: CapabilityImagineClient;
137
- readonly createCodexController?: CreateCodexImageController;
138
- readonly resolveCodexImageRoute?: ResolveCodexImageRoute;
139
- }
140
-
141
- const attachmentRefSchema = {
142
- type: "object" as const,
143
- additionalProperties: false as const,
144
- properties: {
145
- attachmentId: { type: "string" as const, required: true as const },
146
- mediaType: { type: "string" as const, required: true as const },
147
- bytes: { type: "integer" as const, required: true as const },
148
- width: { type: "integer" as const, required: true as const },
149
- height: { type: "integer" as const, required: true as const },
150
- name: { type: "string" as const },
151
- },
152
- };
153
-
154
- /**
155
- * Shared schema fragment for the Imagine video `requestId`. The host value
156
- * schema DSL cannot express string patterns, so execute-time validation keeps
157
- * the authoritative `^[A-Za-z0-9_-]{1,256}$` boundary.
158
- */
159
- const videoRequestIdSchema = {
160
- type: "string" as const,
161
- required: true as const,
162
- };
163
-
164
- /** Shared prompt fragment; execute-time clients reject empty/oversized input. */
165
- const promptParameter = {
166
- type: "string" as const,
167
- required: true as const,
168
- };
169
-
170
- const warningSchema = {
171
- type: "object" as const,
172
- additionalProperties: false as const,
173
- properties: {
174
- index: { type: "integer" as const, required: true as const },
175
- code: { type: "string" as const, required: true as const },
176
- message: { type: "string" as const, required: true as const },
177
- },
178
- };
179
-
180
- const artifactSchema = {
181
- type: "object" as const,
182
- additionalProperties: false as const,
183
- properties: {
184
- artifactId: { type: "string" as const, required: true as const },
185
- mediaType: { type: "string" as const, required: true as const },
186
- bytes: { type: "integer" as const, required: true as const },
187
- createdAt: { type: "integer" as const, required: true as const },
188
- expiresAt: { type: "integer" as const, required: true as const },
189
- name: { type: "string" as const },
190
- },
191
- };
192
-
193
- const imagineImageItemSchema = {
194
- type: "object" as const,
195
- additionalProperties: false as const,
196
- properties: {
197
- attachment: { ...attachmentRefSchema, required: true as const },
198
- path: { type: "string" as const, required: true as const },
199
- },
200
- };
201
-
202
- function disabled(name: string): never {
203
- throw new LlmError(`${name} is disabled`, "INVALID_ARGS");
204
- }
205
-
206
- function imageCountLimit(settings: CapabilitySettings): number {
207
- const value = settings.imageCount;
208
- if (!Number.isSafeInteger(value) || value < CAPABILITY_SETTINGS_BOUNDS.imageCount.min) {
209
- return CAPABILITY_SETTINGS_BOUNDS.imageCount.default;
210
- }
211
- return Math.min(value, CAPABILITY_SETTINGS_BOUNDS.imageCount.max);
212
- }
213
-
214
- function resolveImageCount(n: number | undefined, settings: CapabilitySettings): number {
215
- const limit = imageCountLimit(settings);
216
- const value = n ?? limit;
217
- if (!Number.isSafeInteger(value) || value < 1 || value > limit) {
218
- throw new LlmError(`n cannot exceed the configured imageCount (${String(limit)})`, "INVALID_ARGS");
219
- }
220
- return value;
221
- }
222
-
223
- async function routeFromExec(
224
- exec: ToolRunContext,
225
- resolve: ResolveCodexImageRoute | undefined,
226
- ): Promise<CodexImageRoute | undefined> {
227
- if (resolve === undefined) return undefined;
228
- try {
229
- const resolved = await resolve(exec);
230
- if (resolved === undefined) return undefined;
231
- const modalities = resolved.inputModalities;
232
- return {
233
- ...(typeof resolved.provider === "string" && resolved.provider.length > 0 ? { provider: resolved.provider } : {}),
234
- ...(typeof resolved.model === "string" && resolved.model.length > 0 ? { model: resolved.model } : {}),
235
- ...(Array.isArray(modalities) ? { inputModalities: [...modalities] } : {}),
236
- };
237
- } catch {
238
- return undefined;
239
- }
240
- }
241
-
242
- async function generateSession(
243
- exec: ToolRunContext,
244
- resolve: ResolveCodexImageRoute | undefined,
245
- ): Promise<CodexImageSessionContext> {
246
- const route = await routeFromExec(exec, resolve);
247
- return {
248
- deriveMessages: () => [],
249
- ...(route === undefined ? {} : { route }),
250
- };
251
- }
252
-
253
- async function requireEditSession(
254
- exec: ToolRunContext,
255
- resolve: ResolveCodexImageRoute | undefined,
256
- ): Promise<CodexImageSessionContext> {
257
- const session = exec.agent?.session;
258
- if (session === undefined || typeof session.deriveMessages !== "function") {
259
- throw new LlmError("codex_image_edit requires an active agent session", "INVALID_ARGS");
260
- }
261
- const route = await routeFromExec(exec, resolve);
262
- return {
263
- deriveMessages: () => session.deriveMessages(),
264
- ...(route === undefined ? {} : { route }),
265
- };
266
- }
267
-
268
- function publicPluginPath(path: string | undefined, fallback: string, prefix: string): string {
269
- if (typeof path === "string" && path.startsWith(prefix)) return path;
270
- return fallback;
271
- }
272
-
273
- function publicAttachmentRef(ref: {
274
- attachmentId: string;
275
- mediaType: string;
276
- bytes: number;
277
- width: number;
278
- height: number;
279
- name?: string;
280
- }): ImageAttachmentRef {
281
- return {
282
- attachmentId: ref.attachmentId as ImageAttachmentRef["attachmentId"],
283
- mediaType: ref.mediaType as ImageAttachmentRef["mediaType"],
284
- bytes: ref.bytes,
285
- width: ref.width,
286
- height: ref.height,
287
- ...(ref.name === undefined ? {} : { name: ref.name }),
288
- };
289
- }
290
-
291
- function publicArtifact(meta: MediaArtifactMeta): MediaArtifactMeta {
292
- return {
293
- artifactId: meta.artifactId,
294
- mediaType: meta.mediaType,
295
- bytes: meta.bytes,
296
- createdAt: meta.createdAt,
297
- expiresAt: meta.expiresAt,
298
- ...(meta.name === undefined ? {} : { name: meta.name }),
299
- };
300
- }
301
-
302
- function publicCodexResult<const Operation extends "generate" | "edit">(
303
- result: CodexImageResult & { operation: Operation },
304
- ): {
305
- operation: Operation;
306
- model: typeof CODEX_IMAGE_MODEL;
307
- images: ImageAttachmentRef[];
308
- references: ImageAttachmentRef[];
309
- warnings: Array<{ index: number; code: string; message: string }>;
310
- } {
311
- return {
312
- operation: result.operation,
313
- model: CODEX_IMAGE_MODEL,
314
- images: result.images.map(publicAttachmentRef),
315
- references: result.references.map(publicAttachmentRef),
316
- warnings: result.warnings.map((warning) => ({
317
- index: warning.index,
318
- code: warning.code,
319
- message: warning.message,
320
- })),
321
- };
322
- }
323
-
324
- function publicImagineImageResult(result: ImagineImageResult): {
325
- model: string;
326
- images: Array<{ attachment: ImageAttachmentRef; path: string }>;
327
- attachment: ImageAttachmentRef;
328
- path: string;
329
- } {
330
- const images = result.images.map((image) => {
331
- const attachment = publicAttachmentRef(image.attachment);
332
- return {
333
- attachment,
334
- path: publicPluginPath(image.path, imagineImagePath(String(attachment.attachmentId)), IMAGINE_IMAGE_PREFIX),
335
- };
336
- });
337
- const first = images[0];
338
- const fallback = first ?? {
339
- attachment: publicAttachmentRef(result.attachment),
340
- path: publicPluginPath(result.path, imagineImagePath(String(result.attachment.attachmentId)), IMAGINE_IMAGE_PREFIX),
341
- };
342
- return {
343
- model: GROK_IMAGINE_IMAGE_MODEL,
344
- images,
345
- attachment: fallback.attachment,
346
- path: fallback.path,
347
- };
348
- }
349
-
350
- function publicVideoStart(result: ImagineVideoStartResult): ImagineVideoStartResult {
351
- return {
352
- model: GROK_IMAGINE_VIDEO_MODEL,
353
- requestId: result.requestId,
354
- status: "pending",
355
- };
356
- }
357
-
358
- function publicVideoStatus(result: ImagineVideoStatusResult): {
359
- readonly requestId: string;
360
- readonly status: ImagineVideoStatusResult["status"];
361
- readonly artifact?: MediaArtifactMeta;
362
- readonly path?: string;
363
- readonly error?: string;
364
- } {
365
- const artifact = result.artifact === undefined ? undefined : publicArtifact(result.artifact);
366
- const path =
367
- artifact === undefined
368
- ? undefined
369
- : publicPluginPath(result.path, imagineMediaPath(artifact.artifactId), IMAGINE_MEDIA_PREFIX);
370
- return {
371
- requestId: result.requestId,
372
- status: result.status,
373
- ...(artifact === undefined ? {} : { artifact }),
374
- ...(path === undefined ? {} : { path }),
375
- ...(typeof result.error === "string" ? { error: result.error } : {}),
376
- };
377
- }
378
-
379
- function renderImageRefs(
380
- summary: string,
381
- refs: readonly {
382
- attachmentId: string;
383
- mediaType: string;
384
- bytes: number;
385
- width: number;
386
- height: number;
387
- name?: string;
388
- }[],
389
- ) {
390
- return [
391
- { type: "text" as const, text: summary },
392
- ...refs.map((ref) => ({ type: "image" as const, attachment: publicAttachmentRef(ref) })),
393
- ];
394
- }
395
-
396
- /**
397
- * Build the five optional capability tools. Callers register the returned
398
- * definitions; this function has no Cordis / registry side effects. The tools
399
- * peer is loaded only after Cordis has composed the optional `tools` service.
400
- */
401
- export async function createCapabilityTools(options: CapabilityToolsOptions): Promise<readonly ToolDefinition[]> {
402
- const { defineTool } = await import("@deepseek-ai/dsh-tools");
403
- const createController: CreateCodexImageController =
404
- options.createCodexController ??
405
- ((session, routePolicy) =>
406
- createCodexImageController({
407
- auth: options.auth,
408
- attachments: options.attachments,
409
- session,
410
- routePolicy,
411
- }));
412
-
413
- const generate = defineTool({
414
- name: CODEX_IMAGE_GENERATE_TOOL,
415
- description:
416
- "Generate images with the signed-in Codex subscription. Uses the fixed gpt-image-2 model. Does not accept a model id or image URL.",
417
- parameters: {
418
- prompt: {
419
- ...promptParameter,
420
- description: `Image generation prompt (1-${String(CODEX_IMAGE_PROMPT_MAX_LENGTH)} characters).`,
421
- },
422
- n: {
423
- type: "integer",
424
- enum: [...IMAGE_COUNT_VALUES],
425
- description: "How many images to generate. Defaults to the live imageCount setting and cannot exceed it.",
426
- },
427
- size: { type: "string", enum: [...CODEX_IMAGE_SIZES], description: "Output size. Defaults to auto." },
428
- quality: { type: "string", enum: [...CODEX_IMAGE_QUALITIES], description: "Output quality. Defaults to auto." },
429
- background: {
430
- type: "string",
431
- enum: [...CODEX_IMAGE_BACKGROUNDS],
432
- description: "Background mode. Defaults to auto.",
433
- },
434
- },
435
- output: {
436
- schema: {
437
- type: "object",
438
- additionalProperties: false,
439
- properties: {
440
- operation: { type: "string", required: true, enum: ["generate"] },
441
- model: { type: "string", required: true },
442
- images: { type: "array", required: true, items: attachmentRefSchema },
443
- references: { type: "array", required: true, items: attachmentRefSchema },
444
- warnings: { type: "array", required: true, items: warningSchema },
445
- },
446
- },
447
- render: (_args, value) =>
448
- renderImageRefs(`Generated ${String(value.images.length)} Codex image(s) with ${value.model}.`, value.images),
449
- },
450
- async execute(args, exec) {
451
- const settings = options.current();
452
- if (!settings.codexImages) disabled(CODEX_IMAGE_GENERATE_TOOL);
453
- const n = resolveImageCount(args.n, settings);
454
- const result = await createController(
455
- await generateSession(exec, options.resolveCodexImageRoute),
456
- codexImageRoutePolicy(settings),
457
- ).generate(
458
- {
459
- prompt: args.prompt,
460
- n,
461
- ...(args.size === undefined ? {} : { size: args.size as CodexImageSize }),
462
- ...(args.quality === undefined ? {} : { quality: args.quality as CodexImageQuality }),
463
- ...(args.background === undefined ? {} : { background: args.background as CodexImageBackground }),
464
- },
465
- exec.signal,
466
- );
467
- return publicCodexResult({ ...result, operation: "generate" });
468
- },
469
- });
470
-
471
- const edit = defineTool({
472
- name: CODEX_IMAGE_EDIT_TOOL,
473
- description:
474
- "Edit current-session Codex images. imageIds must be canonical attachment ids visible in this session. Does not accept HTTP(S) URLs or a model id.",
475
- parameters: {
476
- prompt: {
477
- ...promptParameter,
478
- description: `Edit instructions (1-${String(CODEX_IMAGE_PROMPT_MAX_LENGTH)} characters).`,
479
- },
480
- imageIds: {
481
- type: "array",
482
- required: true,
483
- items: { type: "string" },
484
- description: "One to five current-session image attachment ids (optionally image:<id>). URLs are rejected.",
485
- },
486
- n: {
487
- type: "integer",
488
- enum: [...IMAGE_COUNT_VALUES],
489
- description: "How many edited images to return. Defaults to the live imageCount setting and cannot exceed it.",
490
- },
491
- size: { type: "string", enum: [...CODEX_IMAGE_SIZES], description: "Output size. Defaults to auto." },
492
- quality: { type: "string", enum: [...CODEX_IMAGE_QUALITIES], description: "Output quality. Defaults to auto." },
493
- background: {
494
- type: "string",
495
- enum: [...CODEX_IMAGE_BACKGROUNDS],
496
- description: "Background mode. Defaults to auto.",
497
- },
498
- },
499
- output: {
500
- schema: {
501
- type: "object",
502
- additionalProperties: false,
503
- properties: {
504
- operation: { type: "string", required: true, enum: ["edit"] },
505
- model: { type: "string", required: true },
506
- images: { type: "array", required: true, items: attachmentRefSchema },
507
- references: { type: "array", required: true, items: attachmentRefSchema },
508
- warnings: { type: "array", required: true, items: warningSchema },
509
- },
510
- },
511
- render: (_args, value) =>
512
- renderImageRefs(`Edited ${String(value.images.length)} Codex image(s) with ${value.model}.`, value.images),
513
- },
514
- async execute(args, exec) {
515
- const settings = options.current();
516
- if (!settings.codexImageEdits || !settings.codexImages) disabled(CODEX_IMAGE_EDIT_TOOL);
517
- const n = resolveImageCount(args.n, settings);
518
- const result = await createController(
519
- await requireEditSession(exec, options.resolveCodexImageRoute),
520
- codexImageRoutePolicy(settings),
521
- ).edit(
522
- {
523
- prompt: args.prompt,
524
- imageIds: args.imageIds,
525
- n,
526
- ...(args.size === undefined ? {} : { size: args.size as CodexImageSize }),
527
- ...(args.quality === undefined ? {} : { quality: args.quality as CodexImageQuality }),
528
- ...(args.background === undefined ? {} : { background: args.background as CodexImageBackground }),
529
- },
530
- exec.signal,
531
- );
532
- return publicCodexResult({ ...result, operation: "edit" });
533
- },
534
- });
535
-
536
- const imagineImage = defineTool({
537
- name: GROK_IMAGINE_IMAGE_TOOL,
538
- description:
539
- "Generate images with official xAI Imagine. Uses the fixed grok-imagine-image-2.0 model. Does not accept a model id or source URL.",
540
- parameters: {
541
- prompt: {
542
- ...promptParameter,
543
- description: `Image generation prompt (1-${String(IMAGINE_PROMPT_MAX_LENGTH)} characters).`,
544
- },
545
- n: {
546
- type: "integer",
547
- enum: [...IMAGE_COUNT_VALUES],
548
- description: "How many images to generate. Defaults to the live imageCount setting and cannot exceed it.",
549
- },
550
- aspectRatio: {
551
- type: "string",
552
- enum: [...IMAGINE_IMAGE_ASPECT_RATIOS],
553
- description: "Optional Imagine image aspect ratio.",
554
- },
555
- resolution: {
556
- type: "string",
557
- enum: [...IMAGINE_IMAGE_RESOLUTIONS],
558
- description: "Optional Imagine image resolution.",
559
- },
560
- },
561
- output: {
562
- schema: {
563
- type: "object",
564
- additionalProperties: false,
565
- properties: {
566
- model: { type: "string", required: true },
567
- images: { type: "array", required: true, items: imagineImageItemSchema },
568
- attachment: { ...attachmentRefSchema, required: true },
569
- path: { type: "string", required: true },
570
- },
571
- },
572
- render: (_args, value) =>
573
- renderImageRefs(
574
- `Generated ${String(value.images.length)} Imagine image(s).`,
575
- value.images.map((image) => image.attachment),
576
- ),
577
- },
578
- async execute(args, exec) {
579
- const settings = options.current();
580
- if (!settings.grokImagineImage) disabled(GROK_IMAGINE_IMAGE_TOOL);
581
- const input: GenerateImagineImageInput = {
582
- prompt: args.prompt,
583
- n: resolveImageCount(args.n, settings),
584
- ...(args.aspectRatio === undefined ? {} : { aspectRatio: args.aspectRatio as ImagineImageAspectRatio }),
585
- ...(args.resolution === undefined ? {} : { resolution: args.resolution as ImagineImageResolution }),
586
- };
587
- return publicImagineImageResult(await options.imagine.generateImage(input, exec.signal));
588
- },
589
- });
590
-
591
- const imagineVideo = defineTool({
592
- name: GROK_IMAGINE_VIDEO_TOOL,
593
- description:
594
- "Start an official xAI Imagine video job. Uses the fixed grok-imagine-video-1.5 model. Poll status with grok_imagine_video_status and the returned requestId.",
595
- parameters: {
596
- prompt: {
597
- ...promptParameter,
598
- description: `Video generation prompt (1-${String(IMAGINE_PROMPT_MAX_LENGTH)} characters).`,
599
- },
600
- duration: {
601
- type: "integer",
602
- enum: [...VIDEO_DURATION_VALUES],
603
- description: `Optional duration in seconds (${String(IMAGINE_VIDEO_MIN_DURATION_SECONDS)}-${String(IMAGINE_VIDEO_MAX_DURATION_SECONDS)}).`,
604
- },
605
- aspectRatio: {
606
- type: "string",
607
- enum: [...IMAGINE_VIDEO_ASPECT_RATIOS],
608
- description: "Optional Imagine video aspect ratio.",
609
- },
610
- resolution: {
611
- type: "string",
612
- enum: [...IMAGINE_VIDEO_RESOLUTIONS],
613
- description: "Optional Imagine video resolution.",
614
- },
615
- },
616
- output: {
617
- schema: {
618
- type: "object",
619
- additionalProperties: false,
620
- properties: {
621
- model: { type: "string", required: true },
622
- requestId: { ...videoRequestIdSchema },
623
- status: { type: "string", required: true, enum: ["pending"] },
624
- },
625
- },
626
- render: (_args, value) => [{ type: "text", text: `Imagine video job ${value.requestId} is ${value.status}.` }],
627
- },
628
- async execute(args, exec) {
629
- const settings = options.current();
630
- if (!settings.grokImagineVideo) disabled(GROK_IMAGINE_VIDEO_TOOL);
631
- const input: StartImagineVideoInput = {
632
- prompt: args.prompt,
633
- ...(args.duration === undefined ? {} : { duration: args.duration }),
634
- ...(args.aspectRatio === undefined ? {} : { aspectRatio: args.aspectRatio as ImagineVideoAspectRatio }),
635
- ...(args.resolution === undefined ? {} : { resolution: args.resolution as ImagineVideoResolution }),
636
- };
637
- return publicVideoStart(await options.imagine.startVideo(input, exec.signal));
638
- },
639
- });
640
-
641
- const imagineVideoStatus = defineTool({
642
- name: GROK_IMAGINE_VIDEO_STATUS_TOOL,
643
- description:
644
- "Poll a previously started Imagine video job by requestId. Completed results expose only an opaque artifact id and same-origin path.",
645
- parameters: {
646
- requestId: { ...videoRequestIdSchema, description: "Opaque request id returned by grok_imagine_video." },
647
- },
648
- output: {
649
- schema: {
650
- type: "object",
651
- additionalProperties: false,
652
- properties: {
653
- requestId: { ...videoRequestIdSchema },
654
- status: { type: "string", required: true, enum: ["pending", "completed", "failed"] },
655
- artifact: artifactSchema,
656
- path: { type: "string" },
657
- error: { type: "string" },
658
- },
659
- },
660
- render: (_args, value) => {
661
- const detail =
662
- value.status === "completed" && value.path !== undefined
663
- ? `Imagine video ${value.requestId} completed (${value.path}).`
664
- : `Imagine video ${value.requestId} is ${value.status}.`;
665
- return [{ type: "text", text: detail }];
666
- },
667
- },
668
- async execute(args, exec) {
669
- const settings = options.current();
670
- if (!settings.grokImagineVideo) disabled(GROK_IMAGINE_VIDEO_STATUS_TOOL);
671
- const requestId = parseVideoRequestId(args.requestId);
672
- return publicVideoStatus(await options.imagine.videoStatus(requestId, { signal: exec.signal }));
673
- },
674
- });
675
-
676
- return [generate, edit, imagineImage, imagineVideo, imagineVideoStatus];
677
- }
678
-
679
- export const CAPABILITY_TOOL_NAMES = [
680
- CODEX_IMAGE_GENERATE_TOOL,
681
- CODEX_IMAGE_EDIT_TOOL,
682
- GROK_IMAGINE_IMAGE_TOOL,
683
- GROK_IMAGINE_VIDEO_TOOL,
684
- GROK_IMAGINE_VIDEO_STATUS_TOOL,
685
- ] as const;
1
+ /**
2
+ * Optional Codex / Grok Imagine tool definitions. The factory only returns
3
+ * public `ToolDefinition` objects — it never registers on `ctx.tools`.
4
+ * Feature flags and `imageCount` are re-read from `current()` at execute time.
5
+ * @module dsh-coding-subscription-oauth/capability-tools
6
+ */
7
+
8
+ import type { ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
9
+ import { LlmError } from "@deepseek-ai/dsh-llm";
10
+ import type { ToolDefinition, ToolRunContext } from "@deepseek-ai/dsh-tools";
11
+ import { CAPABILITY_SETTINGS_BOUNDS, type CapabilitySettings } from "./capability-settings.ts";
12
+ import type { CodexAuthSession } from "./codex-http.ts";
13
+ import {
14
+ CODEX_IMAGE_BACKGROUNDS,
15
+ CODEX_IMAGE_MODEL,
16
+ CODEX_IMAGE_PROMPT_MAX_LENGTH,
17
+ CODEX_IMAGE_QUALITIES,
18
+ CODEX_IMAGE_SIZES,
19
+ type CodexImageAttachmentStore,
20
+ type CodexImageBackground,
21
+ type CodexImageController,
22
+ type CodexImageQuality,
23
+ type CodexImageResult,
24
+ type CodexImageRoute,
25
+ type CodexImageRoutePolicy,
26
+ type CodexImageSessionContext,
27
+ type CodexImageSize,
28
+ createCodexImageController,
29
+ } from "./codex-images.ts";
30
+ import {
31
+ type GenerateImagineImageInput,
32
+ GROK_IMAGINE_IMAGE_MODEL,
33
+ GROK_IMAGINE_IMAGE_TOOL,
34
+ GROK_IMAGINE_VIDEO_MODEL,
35
+ GROK_IMAGINE_VIDEO_STATUS_TOOL,
36
+ GROK_IMAGINE_VIDEO_TOOL,
37
+ type GrokImagineClient,
38
+ IMAGINE_IMAGE_ASPECT_RATIOS,
39
+ IMAGINE_IMAGE_RESOLUTIONS,
40
+ IMAGINE_PROMPT_MAX_LENGTH,
41
+ IMAGINE_VIDEO_ASPECT_RATIOS,
42
+ IMAGINE_VIDEO_MAX_DURATION_SECONDS,
43
+ IMAGINE_VIDEO_MIN_DURATION_SECONDS,
44
+ IMAGINE_VIDEO_RESOLUTIONS,
45
+ type ImagineImageAspectRatio,
46
+ type ImagineImageResolution,
47
+ type ImagineImageResult,
48
+ type ImagineVideoAspectRatio,
49
+ type ImagineVideoResolution,
50
+ type ImagineVideoStartResult,
51
+ type ImagineVideoStatusResult,
52
+ imagineImagePath,
53
+ parseVideoRequestId,
54
+ type StartImagineVideoInput,
55
+ } from "./grok-imagine.ts";
56
+ import { imagineMediaPath, type MediaArtifactMeta } from "./media-store.ts";
57
+
58
+ export const CODEX_IMAGE_GENERATE_TOOL = "codex_image_generate";
59
+ export const CODEX_IMAGE_EDIT_TOOL = "codex_image_edit";
60
+
61
+ export {
62
+ GROK_IMAGINE_IMAGE_TOOL,
63
+ GROK_IMAGINE_VIDEO_STATUS_TOOL,
64
+ GROK_IMAGINE_VIDEO_TOOL,
65
+ } from "./grok-imagine.ts";
66
+
67
+ const IMAGE_COUNT_VALUES = [1, 2, 3, 4] as const;
68
+ const VIDEO_DURATION_VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as const;
69
+ const IMAGINE_IMAGE_PREFIX = "/plugins/dsh-grok-build/imagine/images/";
70
+ const IMAGINE_MEDIA_PREFIX = "/plugins/dsh-grok-build/imagine/media/";
71
+
72
+ /** Shared client surface; production passes one `GrokImagineClient` so video status can see started jobs. */
73
+ export type CapabilityImagineClient = Pick<GrokImagineClient, "generateImage" | "startVideo" | "videoStatus">;
74
+
75
+ /** Per-exec Codex controller factory. Tests inject a fake; production binds auth + attachments. */
76
+ export type CreateCodexImageController = (
77
+ session: CodexImageSessionContext,
78
+ routePolicy: CodexImageRoutePolicy,
79
+ ) => CodexImageController;
80
+
81
+ /**
82
+ * Route gate for the Codex image tools. `codexImagesAnyModel` relaxes the
83
+ * Codex-route requirement; otherwise the gate stays `codex-capable`.
84
+ */
85
+ export function codexImageRoutePolicy(settings: CapabilitySettings): CodexImageRoutePolicy {
86
+ return settings.codexImagesAnyModel ? "any" : "codex-capable";
87
+ }
88
+
89
+ /** Resolve authoritative host model metadata for the calling route. */
90
+ export type ResolveCodexImageRoute = (exec: ToolRunContext) => Promise<CodexImageRoute | undefined>;
91
+
92
+ /** Exact provider/model identity from the live request header, then agent options. */
93
+ export function callingRouteIdentity(exec: ToolRunContext): { provider: string; model: string } | undefined {
94
+ const session = exec.agent?.session;
95
+ const header =
96
+ session !== undefined && typeof session.requestHeader === "function" ? session.requestHeader()?.config : undefined;
97
+ const provider = header?.provider ?? exec.agent?.options.provider;
98
+ const model = header?.model ?? exec.agent?.options.model;
99
+ if (typeof provider !== "string" || provider.length === 0) return undefined;
100
+ if (typeof model !== "string" || model.length === 0) return undefined;
101
+ return { provider, model };
102
+ }
103
+
104
+ /**
105
+ * Copy host-resolved modalities onto the calling identity. Never invents
106
+ * `inputModalities`; lookup failures keep the identity and omit the field so
107
+ * the image-capability gate fails closed.
108
+ */
109
+ export async function resolveCodexImageRouteFromLlm(
110
+ exec: ToolRunContext,
111
+ resolveModelInfo: (
112
+ provider: string,
113
+ model: string,
114
+ signal?: AbortSignal,
115
+ ) => Promise<{ inputModalities?: readonly string[] }>,
116
+ ): Promise<CodexImageRoute | undefined> {
117
+ const identity = callingRouteIdentity(exec);
118
+ if (identity === undefined) return undefined;
119
+ try {
120
+ const info = await resolveModelInfo(identity.provider, identity.model, exec.signal);
121
+ const modalities = info.inputModalities;
122
+ return {
123
+ ...identity,
124
+ ...(Array.isArray(modalities) ? { inputModalities: [...modalities] } : {}),
125
+ };
126
+ } catch {
127
+ return identity;
128
+ }
129
+ }
130
+
131
+ export interface CapabilityToolsOptions {
132
+ /** Live capability section. Re-read on every execute so a disable takes effect immediately. */
133
+ current(): CapabilitySettings;
134
+ readonly auth: CodexAuthSession;
135
+ readonly attachments: CodexImageAttachmentStore;
136
+ readonly imagine: CapabilityImagineClient;
137
+ readonly createCodexController?: CreateCodexImageController;
138
+ readonly resolveCodexImageRoute?: ResolveCodexImageRoute;
139
+ }
140
+
141
+ const attachmentRefSchema = {
142
+ type: "object" as const,
143
+ additionalProperties: false as const,
144
+ properties: {
145
+ attachmentId: { type: "string" as const, required: true as const },
146
+ mediaType: { type: "string" as const, required: true as const },
147
+ bytes: { type: "integer" as const, required: true as const },
148
+ width: { type: "integer" as const, required: true as const },
149
+ height: { type: "integer" as const, required: true as const },
150
+ name: { type: "string" as const },
151
+ },
152
+ };
153
+
154
+ /**
155
+ * Shared schema fragment for the Imagine video `requestId`. The host value
156
+ * schema DSL cannot express string patterns, so execute-time validation keeps
157
+ * the authoritative `^[A-Za-z0-9_-]{1,256}$` boundary.
158
+ */
159
+ const videoRequestIdSchema = {
160
+ type: "string" as const,
161
+ required: true as const,
162
+ };
163
+
164
+ /** Shared prompt fragment; execute-time clients reject empty/oversized input. */
165
+ const promptParameter = {
166
+ type: "string" as const,
167
+ required: true as const,
168
+ };
169
+
170
+ const warningSchema = {
171
+ type: "object" as const,
172
+ additionalProperties: false as const,
173
+ properties: {
174
+ index: { type: "integer" as const, required: true as const },
175
+ code: { type: "string" as const, required: true as const },
176
+ message: { type: "string" as const, required: true as const },
177
+ },
178
+ };
179
+
180
+ const artifactSchema = {
181
+ type: "object" as const,
182
+ additionalProperties: false as const,
183
+ properties: {
184
+ artifactId: { type: "string" as const, required: true as const },
185
+ mediaType: { type: "string" as const, required: true as const },
186
+ bytes: { type: "integer" as const, required: true as const },
187
+ createdAt: { type: "integer" as const, required: true as const },
188
+ expiresAt: { type: "integer" as const, required: true as const },
189
+ name: { type: "string" as const },
190
+ },
191
+ };
192
+
193
+ const imagineImageItemSchema = {
194
+ type: "object" as const,
195
+ additionalProperties: false as const,
196
+ properties: {
197
+ attachment: { ...attachmentRefSchema, required: true as const },
198
+ path: { type: "string" as const, required: true as const },
199
+ },
200
+ };
201
+
202
+ function disabled(name: string): never {
203
+ throw new LlmError(`${name} is disabled`, "INVALID_ARGS");
204
+ }
205
+
206
+ function imageCountLimit(settings: CapabilitySettings): number {
207
+ const value = settings.imageCount;
208
+ if (!Number.isSafeInteger(value) || value < CAPABILITY_SETTINGS_BOUNDS.imageCount.min) {
209
+ return CAPABILITY_SETTINGS_BOUNDS.imageCount.default;
210
+ }
211
+ return Math.min(value, CAPABILITY_SETTINGS_BOUNDS.imageCount.max);
212
+ }
213
+
214
+ function resolveImageCount(n: number | undefined, settings: CapabilitySettings): number {
215
+ const limit = imageCountLimit(settings);
216
+ const value = n ?? limit;
217
+ if (!Number.isSafeInteger(value) || value < 1 || value > limit) {
218
+ throw new LlmError(`n cannot exceed the configured imageCount (${String(limit)})`, "INVALID_ARGS");
219
+ }
220
+ return value;
221
+ }
222
+
223
+ async function routeFromExec(
224
+ exec: ToolRunContext,
225
+ resolve: ResolveCodexImageRoute | undefined,
226
+ ): Promise<CodexImageRoute | undefined> {
227
+ if (resolve === undefined) return undefined;
228
+ try {
229
+ const resolved = await resolve(exec);
230
+ if (resolved === undefined) return undefined;
231
+ const modalities = resolved.inputModalities;
232
+ return {
233
+ ...(typeof resolved.provider === "string" && resolved.provider.length > 0 ? { provider: resolved.provider } : {}),
234
+ ...(typeof resolved.model === "string" && resolved.model.length > 0 ? { model: resolved.model } : {}),
235
+ ...(Array.isArray(modalities) ? { inputModalities: [...modalities] } : {}),
236
+ };
237
+ } catch {
238
+ return undefined;
239
+ }
240
+ }
241
+
242
+ async function generateSession(
243
+ exec: ToolRunContext,
244
+ resolve: ResolveCodexImageRoute | undefined,
245
+ ): Promise<CodexImageSessionContext> {
246
+ const route = await routeFromExec(exec, resolve);
247
+ return {
248
+ deriveMessages: () => [],
249
+ ...(route === undefined ? {} : { route }),
250
+ };
251
+ }
252
+
253
+ async function requireEditSession(
254
+ exec: ToolRunContext,
255
+ resolve: ResolveCodexImageRoute | undefined,
256
+ ): Promise<CodexImageSessionContext> {
257
+ const session = exec.agent?.session;
258
+ if (session === undefined || typeof session.deriveMessages !== "function") {
259
+ throw new LlmError("codex_image_edit requires an active agent session", "INVALID_ARGS");
260
+ }
261
+ const route = await routeFromExec(exec, resolve);
262
+ return {
263
+ deriveMessages: () => session.deriveMessages(),
264
+ ...(route === undefined ? {} : { route }),
265
+ };
266
+ }
267
+
268
+ function publicPluginPath(path: string | undefined, fallback: string, prefix: string): string {
269
+ if (typeof path === "string" && path.startsWith(prefix)) return path;
270
+ return fallback;
271
+ }
272
+
273
+ function publicAttachmentRef(ref: {
274
+ attachmentId: string;
275
+ mediaType: string;
276
+ bytes: number;
277
+ width: number;
278
+ height: number;
279
+ name?: string;
280
+ }): ImageAttachmentRef {
281
+ return {
282
+ attachmentId: ref.attachmentId as ImageAttachmentRef["attachmentId"],
283
+ mediaType: ref.mediaType as ImageAttachmentRef["mediaType"],
284
+ bytes: ref.bytes,
285
+ width: ref.width,
286
+ height: ref.height,
287
+ ...(ref.name === undefined ? {} : { name: ref.name }),
288
+ };
289
+ }
290
+
291
+ function publicArtifact(meta: MediaArtifactMeta): MediaArtifactMeta {
292
+ return {
293
+ artifactId: meta.artifactId,
294
+ mediaType: meta.mediaType,
295
+ bytes: meta.bytes,
296
+ createdAt: meta.createdAt,
297
+ expiresAt: meta.expiresAt,
298
+ ...(meta.name === undefined ? {} : { name: meta.name }),
299
+ };
300
+ }
301
+
302
+ function publicCodexResult<const Operation extends "generate" | "edit">(
303
+ result: CodexImageResult & { operation: Operation },
304
+ ): {
305
+ operation: Operation;
306
+ model: typeof CODEX_IMAGE_MODEL;
307
+ images: ImageAttachmentRef[];
308
+ references: ImageAttachmentRef[];
309
+ warnings: Array<{ index: number; code: string; message: string }>;
310
+ } {
311
+ return {
312
+ operation: result.operation,
313
+ model: CODEX_IMAGE_MODEL,
314
+ images: result.images.map(publicAttachmentRef),
315
+ references: result.references.map(publicAttachmentRef),
316
+ warnings: result.warnings.map((warning) => ({
317
+ index: warning.index,
318
+ code: warning.code,
319
+ message: warning.message,
320
+ })),
321
+ };
322
+ }
323
+
324
+ function publicImagineImageResult(result: ImagineImageResult): {
325
+ model: string;
326
+ images: Array<{ attachment: ImageAttachmentRef; path: string }>;
327
+ attachment: ImageAttachmentRef;
328
+ path: string;
329
+ } {
330
+ const images = result.images.map((image) => {
331
+ const attachment = publicAttachmentRef(image.attachment);
332
+ return {
333
+ attachment,
334
+ path: publicPluginPath(image.path, imagineImagePath(String(attachment.attachmentId)), IMAGINE_IMAGE_PREFIX),
335
+ };
336
+ });
337
+ const first = images[0];
338
+ const fallback = first ?? {
339
+ attachment: publicAttachmentRef(result.attachment),
340
+ path: publicPluginPath(result.path, imagineImagePath(String(result.attachment.attachmentId)), IMAGINE_IMAGE_PREFIX),
341
+ };
342
+ return {
343
+ model: GROK_IMAGINE_IMAGE_MODEL,
344
+ images,
345
+ attachment: fallback.attachment,
346
+ path: fallback.path,
347
+ };
348
+ }
349
+
350
+ function publicVideoStart(result: ImagineVideoStartResult): ImagineVideoStartResult {
351
+ return {
352
+ model: GROK_IMAGINE_VIDEO_MODEL,
353
+ requestId: result.requestId,
354
+ status: "pending",
355
+ };
356
+ }
357
+
358
+ function publicVideoStatus(result: ImagineVideoStatusResult): {
359
+ readonly requestId: string;
360
+ readonly status: ImagineVideoStatusResult["status"];
361
+ readonly artifact?: MediaArtifactMeta;
362
+ readonly path?: string;
363
+ readonly error?: string;
364
+ } {
365
+ const artifact = result.artifact === undefined ? undefined : publicArtifact(result.artifact);
366
+ const path =
367
+ artifact === undefined
368
+ ? undefined
369
+ : publicPluginPath(result.path, imagineMediaPath(artifact.artifactId), IMAGINE_MEDIA_PREFIX);
370
+ return {
371
+ requestId: result.requestId,
372
+ status: result.status,
373
+ ...(artifact === undefined ? {} : { artifact }),
374
+ ...(path === undefined ? {} : { path }),
375
+ ...(typeof result.error === "string" ? { error: result.error } : {}),
376
+ };
377
+ }
378
+
379
+ function renderImageRefs(
380
+ summary: string,
381
+ refs: readonly {
382
+ attachmentId: string;
383
+ mediaType: string;
384
+ bytes: number;
385
+ width: number;
386
+ height: number;
387
+ name?: string;
388
+ }[],
389
+ ) {
390
+ return [
391
+ { type: "text" as const, text: summary },
392
+ ...refs.map((ref) => ({ type: "image" as const, attachment: publicAttachmentRef(ref) })),
393
+ ];
394
+ }
395
+
396
+ /**
397
+ * Build the five optional capability tools. Callers register the returned
398
+ * definitions; this function has no Cordis / registry side effects. The tools
399
+ * peer is loaded only after Cordis has composed the optional `tools` service.
400
+ */
401
+ export async function createCapabilityTools(options: CapabilityToolsOptions): Promise<readonly ToolDefinition[]> {
402
+ const { defineTool } = await import("@deepseek-ai/dsh-tools");
403
+ const createController: CreateCodexImageController =
404
+ options.createCodexController ??
405
+ ((session, routePolicy) =>
406
+ createCodexImageController({
407
+ auth: options.auth,
408
+ attachments: options.attachments,
409
+ session,
410
+ routePolicy,
411
+ }));
412
+
413
+ const generate = defineTool({
414
+ name: CODEX_IMAGE_GENERATE_TOOL,
415
+ description:
416
+ "Generate images with the signed-in Codex subscription. Uses the fixed gpt-image-2 model. Does not accept a model id or image URL.",
417
+ parameters: {
418
+ prompt: {
419
+ ...promptParameter,
420
+ description: `Image generation prompt (1-${String(CODEX_IMAGE_PROMPT_MAX_LENGTH)} characters).`,
421
+ },
422
+ n: {
423
+ type: "integer",
424
+ enum: [...IMAGE_COUNT_VALUES],
425
+ description: "How many images to generate. Defaults to the live imageCount setting and cannot exceed it.",
426
+ },
427
+ size: { type: "string", enum: [...CODEX_IMAGE_SIZES], description: "Output size. Defaults to auto." },
428
+ quality: { type: "string", enum: [...CODEX_IMAGE_QUALITIES], description: "Output quality. Defaults to auto." },
429
+ background: {
430
+ type: "string",
431
+ enum: [...CODEX_IMAGE_BACKGROUNDS],
432
+ description: "Background mode. Defaults to auto.",
433
+ },
434
+ },
435
+ output: {
436
+ schema: {
437
+ type: "object",
438
+ additionalProperties: false,
439
+ properties: {
440
+ operation: { type: "string", required: true, enum: ["generate"] },
441
+ model: { type: "string", required: true },
442
+ images: { type: "array", required: true, items: attachmentRefSchema },
443
+ references: { type: "array", required: true, items: attachmentRefSchema },
444
+ warnings: { type: "array", required: true, items: warningSchema },
445
+ },
446
+ },
447
+ render: (_args, value) =>
448
+ renderImageRefs(`Generated ${String(value.images.length)} Codex image(s) with ${value.model}.`, value.images),
449
+ },
450
+ async execute(args, exec) {
451
+ const settings = options.current();
452
+ if (!settings.codexImages) disabled(CODEX_IMAGE_GENERATE_TOOL);
453
+ const n = resolveImageCount(args.n, settings);
454
+ const result = await createController(
455
+ await generateSession(exec, options.resolveCodexImageRoute),
456
+ codexImageRoutePolicy(settings),
457
+ ).generate(
458
+ {
459
+ prompt: args.prompt,
460
+ n,
461
+ ...(args.size === undefined ? {} : { size: args.size as CodexImageSize }),
462
+ ...(args.quality === undefined ? {} : { quality: args.quality as CodexImageQuality }),
463
+ ...(args.background === undefined ? {} : { background: args.background as CodexImageBackground }),
464
+ },
465
+ exec.signal,
466
+ );
467
+ return publicCodexResult({ ...result, operation: "generate" });
468
+ },
469
+ });
470
+
471
+ const edit = defineTool({
472
+ name: CODEX_IMAGE_EDIT_TOOL,
473
+ description:
474
+ "Edit current-session Codex images. imageIds must be canonical attachment ids visible in this session. Does not accept HTTP(S) URLs or a model id.",
475
+ parameters: {
476
+ prompt: {
477
+ ...promptParameter,
478
+ description: `Edit instructions (1-${String(CODEX_IMAGE_PROMPT_MAX_LENGTH)} characters).`,
479
+ },
480
+ imageIds: {
481
+ type: "array",
482
+ required: true,
483
+ items: { type: "string" },
484
+ description: "One to five current-session image attachment ids (optionally image:<id>). URLs are rejected.",
485
+ },
486
+ n: {
487
+ type: "integer",
488
+ enum: [...IMAGE_COUNT_VALUES],
489
+ description: "How many edited images to return. Defaults to the live imageCount setting and cannot exceed it.",
490
+ },
491
+ size: { type: "string", enum: [...CODEX_IMAGE_SIZES], description: "Output size. Defaults to auto." },
492
+ quality: { type: "string", enum: [...CODEX_IMAGE_QUALITIES], description: "Output quality. Defaults to auto." },
493
+ background: {
494
+ type: "string",
495
+ enum: [...CODEX_IMAGE_BACKGROUNDS],
496
+ description: "Background mode. Defaults to auto.",
497
+ },
498
+ },
499
+ output: {
500
+ schema: {
501
+ type: "object",
502
+ additionalProperties: false,
503
+ properties: {
504
+ operation: { type: "string", required: true, enum: ["edit"] },
505
+ model: { type: "string", required: true },
506
+ images: { type: "array", required: true, items: attachmentRefSchema },
507
+ references: { type: "array", required: true, items: attachmentRefSchema },
508
+ warnings: { type: "array", required: true, items: warningSchema },
509
+ },
510
+ },
511
+ render: (_args, value) =>
512
+ renderImageRefs(`Edited ${String(value.images.length)} Codex image(s) with ${value.model}.`, value.images),
513
+ },
514
+ async execute(args, exec) {
515
+ const settings = options.current();
516
+ if (!settings.codexImageEdits || !settings.codexImages) disabled(CODEX_IMAGE_EDIT_TOOL);
517
+ const n = resolveImageCount(args.n, settings);
518
+ const result = await createController(
519
+ await requireEditSession(exec, options.resolveCodexImageRoute),
520
+ codexImageRoutePolicy(settings),
521
+ ).edit(
522
+ {
523
+ prompt: args.prompt,
524
+ imageIds: args.imageIds,
525
+ n,
526
+ ...(args.size === undefined ? {} : { size: args.size as CodexImageSize }),
527
+ ...(args.quality === undefined ? {} : { quality: args.quality as CodexImageQuality }),
528
+ ...(args.background === undefined ? {} : { background: args.background as CodexImageBackground }),
529
+ },
530
+ exec.signal,
531
+ );
532
+ return publicCodexResult({ ...result, operation: "edit" });
533
+ },
534
+ });
535
+
536
+ const imagineImage = defineTool({
537
+ name: GROK_IMAGINE_IMAGE_TOOL,
538
+ description:
539
+ "Generate images with official xAI Imagine. Uses the fixed grok-imagine-image-2.0 model. Does not accept a model id or source URL.",
540
+ parameters: {
541
+ prompt: {
542
+ ...promptParameter,
543
+ description: `Image generation prompt (1-${String(IMAGINE_PROMPT_MAX_LENGTH)} characters).`,
544
+ },
545
+ n: {
546
+ type: "integer",
547
+ enum: [...IMAGE_COUNT_VALUES],
548
+ description: "How many images to generate. Defaults to the live imageCount setting and cannot exceed it.",
549
+ },
550
+ aspectRatio: {
551
+ type: "string",
552
+ enum: [...IMAGINE_IMAGE_ASPECT_RATIOS],
553
+ description: "Optional Imagine image aspect ratio.",
554
+ },
555
+ resolution: {
556
+ type: "string",
557
+ enum: [...IMAGINE_IMAGE_RESOLUTIONS],
558
+ description: "Optional Imagine image resolution.",
559
+ },
560
+ },
561
+ output: {
562
+ schema: {
563
+ type: "object",
564
+ additionalProperties: false,
565
+ properties: {
566
+ model: { type: "string", required: true },
567
+ images: { type: "array", required: true, items: imagineImageItemSchema },
568
+ attachment: { ...attachmentRefSchema, required: true },
569
+ path: { type: "string", required: true },
570
+ },
571
+ },
572
+ render: (_args, value) =>
573
+ renderImageRefs(
574
+ `Generated ${String(value.images.length)} Imagine image(s).`,
575
+ value.images.map((image) => image.attachment),
576
+ ),
577
+ },
578
+ async execute(args, exec) {
579
+ const settings = options.current();
580
+ if (!settings.grokImagineImage) disabled(GROK_IMAGINE_IMAGE_TOOL);
581
+ const input: GenerateImagineImageInput = {
582
+ prompt: args.prompt,
583
+ n: resolveImageCount(args.n, settings),
584
+ ...(args.aspectRatio === undefined ? {} : { aspectRatio: args.aspectRatio as ImagineImageAspectRatio }),
585
+ ...(args.resolution === undefined ? {} : { resolution: args.resolution as ImagineImageResolution }),
586
+ };
587
+ return publicImagineImageResult(await options.imagine.generateImage(input, exec.signal));
588
+ },
589
+ });
590
+
591
+ const imagineVideo = defineTool({
592
+ name: GROK_IMAGINE_VIDEO_TOOL,
593
+ description:
594
+ "Start an official xAI Imagine video job. Uses the fixed grok-imagine-video-1.5 model. Poll status with grok_imagine_video_status and the returned requestId.",
595
+ parameters: {
596
+ prompt: {
597
+ ...promptParameter,
598
+ description: `Video generation prompt (1-${String(IMAGINE_PROMPT_MAX_LENGTH)} characters).`,
599
+ },
600
+ duration: {
601
+ type: "integer",
602
+ enum: [...VIDEO_DURATION_VALUES],
603
+ description: `Optional duration in seconds (${String(IMAGINE_VIDEO_MIN_DURATION_SECONDS)}-${String(IMAGINE_VIDEO_MAX_DURATION_SECONDS)}).`,
604
+ },
605
+ aspectRatio: {
606
+ type: "string",
607
+ enum: [...IMAGINE_VIDEO_ASPECT_RATIOS],
608
+ description: "Optional Imagine video aspect ratio.",
609
+ },
610
+ resolution: {
611
+ type: "string",
612
+ enum: [...IMAGINE_VIDEO_RESOLUTIONS],
613
+ description: "Optional Imagine video resolution.",
614
+ },
615
+ },
616
+ output: {
617
+ schema: {
618
+ type: "object",
619
+ additionalProperties: false,
620
+ properties: {
621
+ model: { type: "string", required: true },
622
+ requestId: { ...videoRequestIdSchema },
623
+ status: { type: "string", required: true, enum: ["pending"] },
624
+ },
625
+ },
626
+ render: (_args, value) => [{ type: "text", text: `Imagine video job ${value.requestId} is ${value.status}.` }],
627
+ },
628
+ async execute(args, exec) {
629
+ const settings = options.current();
630
+ if (!settings.grokImagineVideo) disabled(GROK_IMAGINE_VIDEO_TOOL);
631
+ const input: StartImagineVideoInput = {
632
+ prompt: args.prompt,
633
+ ...(args.duration === undefined ? {} : { duration: args.duration }),
634
+ ...(args.aspectRatio === undefined ? {} : { aspectRatio: args.aspectRatio as ImagineVideoAspectRatio }),
635
+ ...(args.resolution === undefined ? {} : { resolution: args.resolution as ImagineVideoResolution }),
636
+ };
637
+ return publicVideoStart(await options.imagine.startVideo(input, exec.signal));
638
+ },
639
+ });
640
+
641
+ const imagineVideoStatus = defineTool({
642
+ name: GROK_IMAGINE_VIDEO_STATUS_TOOL,
643
+ description:
644
+ "Poll a previously started Imagine video job by requestId. Completed results expose only an opaque artifact id and same-origin path.",
645
+ parameters: {
646
+ requestId: { ...videoRequestIdSchema, description: "Opaque request id returned by grok_imagine_video." },
647
+ },
648
+ output: {
649
+ schema: {
650
+ type: "object",
651
+ additionalProperties: false,
652
+ properties: {
653
+ requestId: { ...videoRequestIdSchema },
654
+ status: { type: "string", required: true, enum: ["pending", "completed", "failed"] },
655
+ artifact: artifactSchema,
656
+ path: { type: "string" },
657
+ error: { type: "string" },
658
+ },
659
+ },
660
+ render: (_args, value) => {
661
+ const detail =
662
+ value.status === "completed" && value.path !== undefined
663
+ ? `Imagine video ${value.requestId} completed (${value.path}).`
664
+ : `Imagine video ${value.requestId} is ${value.status}.`;
665
+ return [{ type: "text", text: detail }];
666
+ },
667
+ },
668
+ async execute(args, exec) {
669
+ const settings = options.current();
670
+ if (!settings.grokImagineVideo) disabled(GROK_IMAGINE_VIDEO_STATUS_TOOL);
671
+ const requestId = parseVideoRequestId(args.requestId);
672
+ return publicVideoStatus(await options.imagine.videoStatus(requestId, { signal: exec.signal }));
673
+ },
674
+ });
675
+
676
+ return [generate, edit, imagineImage, imagineVideo, imagineVideoStatus];
677
+ }
678
+
679
+ export const CAPABILITY_TOOL_NAMES = [
680
+ CODEX_IMAGE_GENERATE_TOOL,
681
+ CODEX_IMAGE_EDIT_TOOL,
682
+ GROK_IMAGINE_IMAGE_TOOL,
683
+ GROK_IMAGINE_VIDEO_TOOL,
684
+ GROK_IMAGINE_VIDEO_STATUS_TOOL,
685
+ ] as const;