@d-ai/pi 0.2.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/dist/index.js ADDED
@@ -0,0 +1,1859 @@
1
+ // src/index.ts
2
+ import { Type } from "typebox";
3
+
4
+ // ../typescript/dist/index.js
5
+ var SdkError = class extends Error {
6
+ name = "SdkError";
7
+ };
8
+ var RequestAbortedError = class extends SdkError {
9
+ name = "RequestAbortedError";
10
+ constructor() {
11
+ super("D.AI media request was aborted");
12
+ }
13
+ };
14
+ var RequestTimeoutError = class extends SdkError {
15
+ constructor(timeoutMs) {
16
+ super(`D.AI media request timed out after ${timeoutMs}ms`);
17
+ this.timeoutMs = timeoutMs;
18
+ }
19
+ timeoutMs;
20
+ name = "RequestTimeoutError";
21
+ };
22
+ var ApiError = class extends SdkError {
23
+ constructor(status, message, errorType, code, requestId, bodyPreview = "", raw) {
24
+ super(`${message} (HTTP ${status})`);
25
+ this.status = status;
26
+ this.errorType = errorType;
27
+ this.code = code;
28
+ this.requestId = requestId;
29
+ this.bodyPreview = bodyPreview;
30
+ this.raw = raw;
31
+ }
32
+ status;
33
+ errorType;
34
+ code;
35
+ requestId;
36
+ bodyPreview;
37
+ raw;
38
+ name = "ApiError";
39
+ };
40
+ var WaitTimeoutError = class extends SdkError {
41
+ constructor(jobId, lastResult, maxWaitMs) {
42
+ super(`Job ${jobId} did not finish within ${maxWaitMs}ms`);
43
+ this.jobId = jobId;
44
+ this.lastResult = lastResult;
45
+ this.maxWaitMs = maxWaitMs;
46
+ }
47
+ jobId;
48
+ lastResult;
49
+ maxWaitMs;
50
+ name = "WaitTimeoutError";
51
+ };
52
+ var DEFAULT_BASE_URL = "https://dai.itbug.shop";
53
+ var MAX_BODY_PREVIEW = 2e3;
54
+ var RESERVED_HEADERS = {
55
+ authorization: true,
56
+ "content-type": true
57
+ };
58
+ var Transport = class {
59
+ baseUrl;
60
+ timeoutMs;
61
+ #apiKey;
62
+ #headers;
63
+ #fetch;
64
+ constructor(configuration) {
65
+ if (!configuration.apiKey.trim()) {
66
+ throw new SdkError("apiKey is required");
67
+ }
68
+ this.#apiKey = configuration.apiKey;
69
+ this.baseUrl = normalizeBaseUrl(configuration.baseUrl ?? DEFAULT_BASE_URL);
70
+ this.timeoutMs = configuration.timeoutMs ?? 6e4;
71
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
72
+ throw new SdkError("timeoutMs must be greater than zero");
73
+ }
74
+ this.#headers = safeHeaders(configuration.headers);
75
+ const fetchImplementation = configuration.fetch ?? globalThis.fetch;
76
+ if (typeof fetchImplementation !== "function") {
77
+ throw new SdkError("A Fetch API implementation is required");
78
+ }
79
+ this.#fetch = fetchImplementation.bind(globalThis);
80
+ }
81
+ async request(method, path, options = {}) {
82
+ const url = endpointUrl(this.baseUrl, path, options.query);
83
+ const headers = new Headers(this.#headers);
84
+ mergeSafeHeaders(headers, options.headers);
85
+ headers.set("Authorization", `Bearer ${this.#apiKey}`);
86
+ if (options.body !== void 0) {
87
+ headers.set("Content-Type", "application/json");
88
+ }
89
+ const controller = new AbortController();
90
+ let timedOut = false;
91
+ const abortFromCaller = () => controller.abort(options.signal?.reason);
92
+ if (options.signal?.aborted) {
93
+ throw new RequestAbortedError();
94
+ }
95
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
96
+ const timeout = setTimeout(() => {
97
+ timedOut = true;
98
+ controller.abort();
99
+ }, this.timeoutMs);
100
+ try {
101
+ const init = {
102
+ method,
103
+ headers,
104
+ signal: controller.signal
105
+ };
106
+ if (options.body !== void 0) {
107
+ init.body = JSON.stringify(options.body);
108
+ }
109
+ const response = await this.#fetch(url, init);
110
+ return await parseResponse(response);
111
+ } catch (error) {
112
+ if (timedOut) {
113
+ throw new RequestTimeoutError(this.timeoutMs);
114
+ }
115
+ if (options.signal?.aborted || isAbortError(error)) {
116
+ throw new RequestAbortedError();
117
+ }
118
+ if (error instanceof SdkError) {
119
+ throw error;
120
+ }
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ throw new SdkError(`D.AI media network request failed: ${message}`);
123
+ } finally {
124
+ clearTimeout(timeout);
125
+ options.signal?.removeEventListener("abort", abortFromCaller);
126
+ }
127
+ }
128
+ toString() {
129
+ return `Transport(baseUrl=${JSON.stringify(this.baseUrl)}, apiKey="[REDACTED]")`;
130
+ }
131
+ };
132
+ function normalizeBaseUrl(value) {
133
+ let url;
134
+ try {
135
+ url = new URL(value.trim());
136
+ } catch {
137
+ throw new SdkError("baseUrl must be an absolute http or https URL");
138
+ }
139
+ if (url.protocol !== "http:" && url.protocol !== "https:" || !url.hostname) {
140
+ throw new SdkError("baseUrl must be an absolute http or https URL");
141
+ }
142
+ if (url.username || url.password || url.search || url.hash) {
143
+ throw new SdkError("baseUrl cannot contain user info, query, or fragment");
144
+ }
145
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/`;
146
+ return url.toString();
147
+ }
148
+ function endpointUrl(baseUrl, path, query) {
149
+ if (!path.startsWith("/") || path.includes("?") || path.includes("#")) {
150
+ throw new SdkError(`Invalid endpoint path: ${path}`);
151
+ }
152
+ const url = new URL(`${baseUrl.replace(/\/+$/, "")}${path}`);
153
+ for (const [key, value] of Object.entries(query ?? {})) {
154
+ url.searchParams.set(key, value);
155
+ }
156
+ return url;
157
+ }
158
+ async function parseResponse(response) {
159
+ const requestId = response.headers.get("x-request-id") ?? void 0;
160
+ const bodyText = await response.text();
161
+ let parsed;
162
+ try {
163
+ parsed = JSON.parse(bodyText);
164
+ } catch {
165
+ if (response.ok) {
166
+ throw new SdkError("D.AI response was not valid JSON");
167
+ }
168
+ throw new ApiError(
169
+ response.status,
170
+ "D.AI media request failed",
171
+ void 0,
172
+ void 0,
173
+ requestId,
174
+ bodyText.slice(0, MAX_BODY_PREVIEW)
175
+ );
176
+ }
177
+ if (!isJsonObject(parsed)) {
178
+ throw new SdkError("D.AI response must be a JSON object");
179
+ }
180
+ if (!response.ok) {
181
+ const error = isJsonObject(parsed.error) ? parsed.error : void 0;
182
+ const message = stringValue(error?.message) ?? stringValue(parsed.message) ?? "D.AI media request failed";
183
+ throw new ApiError(
184
+ response.status,
185
+ message,
186
+ stringValue(error?.type),
187
+ stringValue(error?.code),
188
+ requestId,
189
+ bodyText.slice(0, MAX_BODY_PREVIEW),
190
+ parsed
191
+ );
192
+ }
193
+ return requestId === void 0 ? { body: parsed } : { body: parsed, requestId };
194
+ }
195
+ function safeHeaders(value) {
196
+ const headers = new Headers();
197
+ mergeSafeHeaders(headers, value, true);
198
+ return headers;
199
+ }
200
+ function mergeSafeHeaders(target, value, rejectReserved = false) {
201
+ if (value === void 0) {
202
+ return;
203
+ }
204
+ for (const [name, headerValue] of new Headers(value)) {
205
+ if (RESERVED_HEADERS[name.toLowerCase()] === true) {
206
+ if (rejectReserved) {
207
+ throw new SdkError(`Header is reserved and managed by the SDK: ${name}`);
208
+ }
209
+ continue;
210
+ }
211
+ target.set(name, headerValue);
212
+ }
213
+ }
214
+ function isAbortError(error) {
215
+ return error instanceof DOMException ? error.name === "AbortError" : error instanceof Error && error.name === "AbortError";
216
+ }
217
+ function isJsonObject(value) {
218
+ return typeof value === "object" && value !== null && !Array.isArray(value);
219
+ }
220
+ function stringValue(value) {
221
+ return typeof value === "string" ? value : void 0;
222
+ }
223
+ function mergePayload(extra, typed) {
224
+ const result = { ...extra ?? {} };
225
+ for (const [key, value] of Object.entries(typed)) {
226
+ if (value !== void 0) {
227
+ result[key] = value;
228
+ }
229
+ }
230
+ return result;
231
+ }
232
+ function imageResult(response) {
233
+ const images = [];
234
+ const data = response.body.data;
235
+ if (Array.isArray(data)) {
236
+ for (const item of data) {
237
+ if (!isJsonObject(item)) {
238
+ continue;
239
+ }
240
+ const url = stringValue(item.url);
241
+ const b64Json = stringValue(item.b64_json);
242
+ if (url !== void 0 || b64Json !== void 0) {
243
+ images.push({
244
+ ...url === void 0 ? {} : { url },
245
+ ...b64Json === void 0 ? {} : { b64Json }
246
+ });
247
+ }
248
+ }
249
+ } else if (isJsonObject(data)) {
250
+ const url = stringValue(data.image_url);
251
+ if (url !== void 0) {
252
+ images.push({ url });
253
+ }
254
+ }
255
+ return {
256
+ images,
257
+ ...response.requestId === void 0 ? {} : { requestId: response.requestId },
258
+ raw: response.body
259
+ };
260
+ }
261
+ function jobHandle(response, operation, fetchPath) {
262
+ const jobId = extractJobId(response.body);
263
+ const serverFetchUrl = stringValue(response.body.fetchUrl);
264
+ return {
265
+ jobId,
266
+ operation,
267
+ fetchPath,
268
+ ...serverFetchUrl === void 0 ? {} : { serverFetchUrl },
269
+ ...response.requestId === void 0 ? {} : { requestId: response.requestId },
270
+ raw: response.body
271
+ };
272
+ }
273
+ function jobResult(handle, response) {
274
+ const raw = response.body;
275
+ const data = isJsonObject(raw.data) ? raw.data : void 0;
276
+ const output = isJsonObject(raw.output) ? raw.output : void 0;
277
+ const error = isJsonObject(raw.error) ? raw.error : void 0;
278
+ const rawStatus = stringValue(raw.status) ?? stringValue(data?.status) ?? stringValue(output?.task_status);
279
+ const assets = /* @__PURE__ */ new Set();
280
+ collectAssets(raw, assets);
281
+ const errorMessage = stringValue(raw.message) ?? stringValue(error?.message);
282
+ return {
283
+ jobId: handle.jobId,
284
+ state: normalizeJobState(rawStatus),
285
+ assets: [...assets],
286
+ ...errorMessage === void 0 ? {} : { errorMessage },
287
+ ...response.requestId === void 0 ? {} : { requestId: response.requestId },
288
+ raw
289
+ };
290
+ }
291
+ function normalizeJobState(value) {
292
+ switch (value?.trim().toLowerCase()) {
293
+ case "queued":
294
+ case "pending":
295
+ case "submitted":
296
+ return "queued";
297
+ case "running":
298
+ case "processing":
299
+ case "in_progress":
300
+ return "running";
301
+ case "success":
302
+ case "succeeded":
303
+ case "completed":
304
+ case "finished":
305
+ return "succeeded";
306
+ case "failed":
307
+ case "error":
308
+ case "cancelled":
309
+ case "canceled":
310
+ return "failed";
311
+ default:
312
+ return "unknown";
313
+ }
314
+ }
315
+ function isTerminalState(state) {
316
+ return state === "succeeded" || state === "failed";
317
+ }
318
+ function extractJobId(body) {
319
+ const data = isJsonObject(body.data) ? body.data : void 0;
320
+ const values = [body.jobId, body.job_id, data?.jobId, data?.job_id];
321
+ for (const value of values) {
322
+ const jobId = stringValue(value)?.trim();
323
+ if (jobId) {
324
+ return jobId;
325
+ }
326
+ }
327
+ throw new SdkError(
328
+ `Asynchronous response did not include jobId; body: ${JSON.stringify(body).slice(0, 2e3)}`
329
+ );
330
+ }
331
+ function collectAssets(value, assets) {
332
+ if (Array.isArray(value)) {
333
+ for (const item of value) {
334
+ collectAssets(item, assets);
335
+ }
336
+ return;
337
+ }
338
+ if (!isJsonObject(value)) {
339
+ return;
340
+ }
341
+ for (const key of ["url", "image_url", "video_url", "audio_url"]) {
342
+ const candidate = stringValue(value[key]);
343
+ if (candidate !== void 0) {
344
+ assets.add(candidate);
345
+ }
346
+ }
347
+ for (const child of Object.values(value)) {
348
+ collectAssets(child, assets);
349
+ }
350
+ }
351
+ var AudioApi = class {
352
+ suno;
353
+ constructor(transport) {
354
+ this.suno = new SunoAudioApi(transport);
355
+ }
356
+ };
357
+ var SunoAudioApi = class {
358
+ constructor(transport) {
359
+ this.transport = transport;
360
+ }
361
+ transport;
362
+ async music(request, options = {}) {
363
+ const body = mergePayload(request.extra, {
364
+ custom: request.custom,
365
+ instrumental: request.instrumental,
366
+ mv: request.mv,
367
+ ...request.gptDescriptionPrompt === void 0 ? {} : { gpt_description_prompt: request.gptDescriptionPrompt },
368
+ ...request.prompt === void 0 ? {} : { prompt: request.prompt },
369
+ ...request.title === void 0 ? {} : { title: request.title },
370
+ ...request.tags === void 0 ? {} : { tags: request.tags },
371
+ ...request.negativeTags === void 0 ? {} : { negative_tags: request.negativeTags },
372
+ ...request.styleWeight === void 0 ? {} : { style_weight: request.styleWeight },
373
+ ...request.weirdnessConstraint === void 0 ? {} : { weirdness_constraint: request.weirdnessConstraint },
374
+ ...request.audioWeight === void 0 ? {} : { audio_weight: request.audioWeight },
375
+ ...request.autoLyrics === void 0 ? {} : { auto_lyrics: request.autoLyrics },
376
+ ...request.vocalGender === void 0 ? {} : { vocal_gender: request.vocalGender },
377
+ ...request.personaId === void 0 ? {} : { persona_id: request.personaId },
378
+ ...request.isStorage === void 0 ? {} : { is_storage: request.isStorage }
379
+ });
380
+ const response = await this.transport.request("POST", "/api/audio/suno/v1/music", {
381
+ ...options,
382
+ body
383
+ });
384
+ return jobHandle(response, "suno_music", "/api/audio/suno/v2/fetch");
385
+ }
386
+ };
387
+ var ImagesApi = class {
388
+ openai;
389
+ gemini;
390
+ novel;
391
+ midjourney;
392
+ flux;
393
+ constructor(transport) {
394
+ this.openai = new OpenAiImagesApi(transport);
395
+ this.gemini = new GeminiImagesApi(transport);
396
+ this.novel = new NovelImagesApi(transport);
397
+ this.midjourney = new MidjourneyImagesApi(transport);
398
+ this.flux = new FluxImagesApi(transport);
399
+ }
400
+ };
401
+ var OpenAiImagesApi = class {
402
+ constructor(transport) {
403
+ this.transport = transport;
404
+ }
405
+ transport;
406
+ async generate(request, options = {}) {
407
+ const body = mergePayload(request.extra, {
408
+ prompt: request.prompt,
409
+ ...request.model === void 0 ? {} : { model: request.model },
410
+ ...request.size === void 0 ? {} : { size: request.size },
411
+ ...request.quality === void 0 ? {} : { quality: request.quality },
412
+ ...request.n === void 0 ? {} : { n: request.n },
413
+ ...request.responseFormat === void 0 ? {} : { response_format: request.responseFormat },
414
+ ...request.background === void 0 ? {} : { background: request.background },
415
+ ...request.outputFormat === void 0 ? {} : { output_format: request.outputFormat }
416
+ });
417
+ const response = await this.transport.request(
418
+ "POST",
419
+ "/api/image/openai/v1/images/generations",
420
+ { ...options, body }
421
+ );
422
+ return imageResult(response);
423
+ }
424
+ async gptGenerate(request, options = {}) {
425
+ const body = mergePayload(request.extra, {
426
+ prompt: request.prompt,
427
+ ...request.model === void 0 ? {} : { model: request.model },
428
+ ...request.size === void 0 ? {} : { size: request.size },
429
+ ...request.quality === void 0 ? {} : { quality: request.quality },
430
+ ...request.n === void 0 ? {} : { n: request.n },
431
+ ...request.responseFormat === void 0 ? {} : { response_format: request.responseFormat },
432
+ ...request.background === void 0 ? {} : { background: request.background },
433
+ ...request.outputFormat === void 0 ? {} : { output_format: request.outputFormat }
434
+ });
435
+ const response = await this.transport.request(
436
+ "POST",
437
+ "/api/image/openai/gpt/generations",
438
+ { ...options, body }
439
+ );
440
+ return jobHandle(response, "openai_gpt_image", "/api/image/openai/gpt/fetch");
441
+ }
442
+ };
443
+ var FluxImagesApi = class {
444
+ constructor(transport) {
445
+ this.transport = transport;
446
+ }
447
+ transport;
448
+ async generate(request, options = {}) {
449
+ const body = mergePayload(request.extra, {
450
+ prompt: request.prompt,
451
+ ...request.model === void 0 ? {} : { model: request.model },
452
+ ...request.size === void 0 ? {} : { size: request.size }
453
+ });
454
+ const response = await this.transport.request("POST", "/api/image/flux/v1/generate", {
455
+ ...options,
456
+ body
457
+ });
458
+ return jobHandle(response, "flux_image", "/api/image/flux/v1/fetch");
459
+ }
460
+ async edits(request, options = {}) {
461
+ const body = mergePayload(request.extra, {
462
+ prompt: request.prompt,
463
+ image: request.image,
464
+ ...request.model === void 0 ? {} : { model: request.model },
465
+ ...request.size === void 0 ? {} : { size: request.size }
466
+ });
467
+ const response = await this.transport.request("POST", "/api/image/flux/v1/edits", {
468
+ ...options,
469
+ body
470
+ });
471
+ return jobHandle(response, "flux_image", "/api/image/flux/v1/fetch");
472
+ }
473
+ };
474
+ var GeminiImagesApi = class {
475
+ constructor(transport) {
476
+ this.transport = transport;
477
+ }
478
+ transport;
479
+ async generate(request, options = {}) {
480
+ const body = mergePayload(request.extra, {
481
+ model: request.model,
482
+ prompt: request.prompt,
483
+ ...request.aspectRatio === void 0 ? {} : { aspect_ratio: request.aspectRatio },
484
+ reference_images: request.referenceImages ?? []
485
+ });
486
+ const response = await this.transport.request("POST", "/api/image/gemini/generate", {
487
+ ...options,
488
+ body
489
+ });
490
+ return imageResult(response);
491
+ }
492
+ };
493
+ var NovelImagesApi = class {
494
+ constructor(transport) {
495
+ this.transport = transport;
496
+ }
497
+ transport;
498
+ async textToImage(request, options = {}) {
499
+ const response = await this.transport.request(
500
+ "POST",
501
+ "/api/image/novel/v1/text-to-image",
502
+ { ...options, body: novelPayload(request) }
503
+ );
504
+ return jobHandle(response, "novel_text_to_image", "/api/image/novel/v1/fetch");
505
+ }
506
+ async imageToImage(request, options = {}) {
507
+ const body = mergePayload(novelPayload(request), {
508
+ image: request.image,
509
+ ...request.strength === void 0 ? {} : { strength: request.strength },
510
+ ...request.noise === void 0 ? {} : { noise: request.noise }
511
+ });
512
+ const response = await this.transport.request(
513
+ "POST",
514
+ "/api/image/novel/v1/image-to-image",
515
+ { ...options, body }
516
+ );
517
+ return jobHandle(response, "novel_image_to_image", "/api/image/novel/v1/fetch");
518
+ }
519
+ };
520
+ var MidjourneyImagesApi = class {
521
+ constructor(transport) {
522
+ this.transport = transport;
523
+ }
524
+ transport;
525
+ async imagine(request, options = {}) {
526
+ return await this.submit(
527
+ "/api/image/midjourney/v1/imagine",
528
+ "midjourney_imagine",
529
+ mergePayload(request.extra, {
530
+ prompt: request.prompt,
531
+ ...request.mode === void 0 ? {} : { mode: request.mode }
532
+ }),
533
+ options
534
+ );
535
+ }
536
+ async action(request, options = {}) {
537
+ return await this.submit(
538
+ "/api/image/midjourney/v1/action",
539
+ "midjourney_action",
540
+ mergePayload(request.extra, { jobId: request.jobId, action: request.action }),
541
+ options
542
+ );
543
+ }
544
+ async blend(request, options = {}) {
545
+ return await this.submit(
546
+ "/api/image/midjourney/v1/blend",
547
+ "midjourney_blend",
548
+ mergePayload(request.extra, {
549
+ imageUrls: request.imageUrls,
550
+ ...request.mode === void 0 ? {} : { mode: request.mode }
551
+ }),
552
+ options
553
+ );
554
+ }
555
+ async describe(request, options = {}) {
556
+ return await this.submit(
557
+ "/api/image/midjourney/v1/describe",
558
+ "midjourney_describe",
559
+ mergePayload(request.extra, { imageUrl: request.imageUrl }),
560
+ options
561
+ );
562
+ }
563
+ async seed(request, options = {}) {
564
+ return await this.submit(
565
+ "/api/image/midjourney/v1/seed",
566
+ "midjourney_seed",
567
+ mergePayload(request.extra, { jobId: request.jobId }),
568
+ options
569
+ );
570
+ }
571
+ async inpaint(request, options = {}) {
572
+ return await this.submit(
573
+ "/api/image/midjourney/v1/inpaint",
574
+ "midjourney_inpaint",
575
+ mergePayload(request.extra, {
576
+ jobId: request.jobId,
577
+ prompt: request.prompt,
578
+ mask: request.mask
579
+ }),
580
+ options
581
+ );
582
+ }
583
+ async submit(path, operation, body, options) {
584
+ const response = await this.transport.request("POST", path, { ...options, body });
585
+ return jobHandle(response, operation, "/api/image/midjourney/v1/fetch");
586
+ }
587
+ };
588
+ function novelPayload(request) {
589
+ return mergePayload(request.extra, {
590
+ prompt: request.prompt,
591
+ ...request.model === void 0 ? {} : { model: request.model },
592
+ ...request.n === void 0 ? {} : { n: request.n },
593
+ ...request.width === void 0 ? {} : { width: request.width },
594
+ ...request.height === void 0 ? {} : { height: request.height },
595
+ ...request.seed === void 0 ? {} : { seed: request.seed },
596
+ ...request.negativePrompt === void 0 ? {} : { negative_prompt: request.negativePrompt },
597
+ ...request.steps === void 0 ? {} : { steps: request.steps },
598
+ ...request.scale === void 0 ? {} : { scale: request.scale },
599
+ ...request.sampler === void 0 ? {} : { sampler: request.sampler },
600
+ ...request.noiseSchedule === void 0 ? {} : { noise_schedule: request.noiseSchedule }
601
+ });
602
+ }
603
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
604
+ var DEFAULT_MAX_WAIT_MS = 20 * 6e4;
605
+ var JobsApi = class {
606
+ constructor(transport) {
607
+ this.transport = transport;
608
+ }
609
+ transport;
610
+ async fetch(handle, options = {}) {
611
+ const response = await this.transport.request("GET", handle.fetchPath, {
612
+ ...options,
613
+ query: { jobId: handle.jobId }
614
+ });
615
+ return jobResult(handle, response);
616
+ }
617
+ async wait(handle, options = {}) {
618
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
619
+ const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
620
+ if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
621
+ throw new SdkError("pollIntervalMs must be greater than zero");
622
+ }
623
+ if (!Number.isFinite(maxWaitMs) || maxWaitMs <= 0) {
624
+ throw new SdkError("maxWaitMs must be greater than zero");
625
+ }
626
+ const startedAt = Date.now();
627
+ while (true) {
628
+ const result = await this.fetch(handle, options);
629
+ if (isTerminalState(result.state)) {
630
+ return result;
631
+ }
632
+ const elapsed = Date.now() - startedAt;
633
+ if (elapsed >= maxWaitMs) {
634
+ throw new WaitTimeoutError(handle.jobId, result, maxWaitMs);
635
+ }
636
+ await abortableDelay(Math.min(pollIntervalMs, maxWaitMs - elapsed), options.signal);
637
+ }
638
+ }
639
+ };
640
+ async function abortableDelay(milliseconds, signal) {
641
+ if (signal?.aborted) {
642
+ throw new RequestAbortedError();
643
+ }
644
+ await new Promise((resolve, reject) => {
645
+ const complete = () => {
646
+ signal?.removeEventListener("abort", abort);
647
+ resolve();
648
+ };
649
+ const timer = setTimeout(complete, milliseconds);
650
+ const abort = () => {
651
+ clearTimeout(timer);
652
+ signal?.removeEventListener("abort", abort);
653
+ reject(new RequestAbortedError());
654
+ };
655
+ signal?.addEventListener("abort", abort, { once: true });
656
+ });
657
+ }
658
+ var VideosApi = class {
659
+ gemini;
660
+ midjourney;
661
+ happyhorse;
662
+ flux;
663
+ constructor(transport) {
664
+ this.gemini = new GeminiVideosApi(transport);
665
+ this.midjourney = new MidjourneyVideosApi(transport);
666
+ this.happyhorse = new HappyHorseVideosApi(transport);
667
+ this.flux = new FluxVideosApi(transport);
668
+ }
669
+ };
670
+ var GeminiVideosApi = class {
671
+ constructor(transport) {
672
+ this.transport = transport;
673
+ }
674
+ transport;
675
+ async generate(request, options = {}) {
676
+ const body = mergePayload(request.extra, {
677
+ prompt: request.prompt,
678
+ model: request.model,
679
+ aspect_ratio: request.aspectRatio,
680
+ ...request.resolution === void 0 ? {} : { resolution: request.resolution },
681
+ ...request.duration === void 0 ? {} : { duration: request.duration },
682
+ ...request.generationType === void 0 ? {} : { generation_type: request.generationType },
683
+ reference_images: request.referenceImages ?? [],
684
+ reference_videos: request.referenceVideos ?? []
685
+ });
686
+ const response = await this.transport.request("POST", "/api/video/gemini/generate", {
687
+ ...options,
688
+ body
689
+ });
690
+ return jobHandle(response, "gemini_video", "/api/video/gemini/fetch");
691
+ }
692
+ };
693
+ var FluxVideosApi = class {
694
+ constructor(transport) {
695
+ this.transport = transport;
696
+ }
697
+ transport;
698
+ async generate(request, options = {}) {
699
+ const body = mergePayload(request.extra, {
700
+ prompt: request.prompt,
701
+ ...request.model === void 0 ? {} : { model: request.model },
702
+ ...request.mode === void 0 ? {} : { mode: request.mode },
703
+ ...request.aspectRatio === void 0 ? {} : { aspect_ratio: request.aspectRatio },
704
+ ...request.duration === void 0 ? {} : { duration: request.duration },
705
+ ...request.resolution === void 0 ? {} : { resolution: request.resolution },
706
+ ...request.generateAudio === void 0 ? {} : { generate_audio: request.generateAudio },
707
+ ...request.keyframes === void 0 ? {} : { keyframes: request.keyframes },
708
+ ...request.startVideo === void 0 ? {} : { start_video: request.startVideo },
709
+ ...request.draft === void 0 ? {} : { draft: request.draft }
710
+ });
711
+ const response = await this.transport.request("POST", "/api/video/flux/v1/generate", {
712
+ ...options,
713
+ body
714
+ });
715
+ return jobHandle(response, "flux_video", "/api/video/flux/v1/fetch");
716
+ }
717
+ };
718
+ var MidjourneyVideosApi = class {
719
+ constructor(transport) {
720
+ this.transport = transport;
721
+ }
722
+ transport;
723
+ async submit(request, options = {}) {
724
+ const body = mergePayload(request.extra, {
725
+ prompt: request.prompt,
726
+ ...request.imageUrl === void 0 ? {} : { imageUrl: request.imageUrl },
727
+ ...request.model === void 0 ? {} : { model: request.model },
728
+ ...request.manual === void 0 ? {} : { manual: request.manual },
729
+ ...request.resolution === void 0 ? {} : { resolution: request.resolution },
730
+ ...request.bs === void 0 ? {} : { bs: request.bs }
731
+ });
732
+ const response = await this.transport.request(
733
+ "POST",
734
+ "/api/video/midjourney/v1/submit",
735
+ { ...options, body }
736
+ );
737
+ return jobHandle(response, "midjourney_video", "/api/video/midjourney/v1/fetch");
738
+ }
739
+ async extend(request, options = {}) {
740
+ const body = mergePayload(request.extra, {
741
+ jobId: request.jobId,
742
+ index: request.index,
743
+ ...request.animateMode === void 0 ? {} : { animateMode: request.animateMode },
744
+ ...request.manual === void 0 ? {} : { manual: request.manual },
745
+ ...request.bs === void 0 ? {} : { bs: request.bs }
746
+ });
747
+ const response = await this.transport.request(
748
+ "POST",
749
+ "/api/video/midjourney/v1/extend",
750
+ { ...options, body }
751
+ );
752
+ return jobHandle(
753
+ response,
754
+ "midjourney_video_extend",
755
+ "/api/video/midjourney/v1/fetch"
756
+ );
757
+ }
758
+ };
759
+ var HappyHorseVideosApi = class {
760
+ constructor(transport) {
761
+ this.transport = transport;
762
+ }
763
+ transport;
764
+ async textToVideo(request, options = {}) {
765
+ return await this.submit(
766
+ "/api/video/happyhorse/text-to-video",
767
+ "happyhorse_text_to_video",
768
+ request,
769
+ options
770
+ );
771
+ }
772
+ async imageToVideo(request, options = {}) {
773
+ return await this.submit(
774
+ "/api/video/happyhorse/image-to-video",
775
+ "happyhorse_image_to_video",
776
+ request,
777
+ options
778
+ );
779
+ }
780
+ async referenceToVideo(request, options = {}) {
781
+ return await this.submit(
782
+ "/api/video/happyhorse/reference-to-video",
783
+ "happyhorse_reference_to_video",
784
+ request,
785
+ options
786
+ );
787
+ }
788
+ async videoEdit(request, options = {}) {
789
+ return await this.submit(
790
+ "/api/video/happyhorse/video-edit",
791
+ "happyhorse_video_edit",
792
+ request,
793
+ options
794
+ );
795
+ }
796
+ async submit(path, operation, request, options) {
797
+ const body = mergePayload(request.extra, {
798
+ model: request.model,
799
+ input: {
800
+ ...request.input.prompt === void 0 ? {} : { prompt: request.input.prompt },
801
+ media: (request.input.media ?? []).map((item) => ({ type: item.type, url: item.url }))
802
+ },
803
+ parameters: request.parameters ?? {}
804
+ });
805
+ const response = await this.transport.request("POST", path, { ...options, body });
806
+ return jobHandle(response, operation, "/api/video/happyhorse/fetch");
807
+ }
808
+ };
809
+ var DaiMediaClient = class {
810
+ images;
811
+ videos;
812
+ audio;
813
+ jobs;
814
+ baseUrl;
815
+ timeoutMs;
816
+ #transport;
817
+ constructor(configuration) {
818
+ this.#transport = new Transport(configuration);
819
+ this.images = new ImagesApi(this.#transport);
820
+ this.videos = new VideosApi(this.#transport);
821
+ this.audio = new AudioApi(this.#transport);
822
+ this.jobs = new JobsApi(this.#transport);
823
+ this.baseUrl = this.#transport.baseUrl;
824
+ this.timeoutMs = this.#transport.timeoutMs;
825
+ }
826
+ toString() {
827
+ return `DaiMediaClient(baseUrl=${JSON.stringify(this.baseUrl)}, apiKey="[REDACTED]")`;
828
+ }
829
+ };
830
+
831
+ // src/client.ts
832
+ function createMediaClient(config, options = {}) {
833
+ return new DaiMediaClient({
834
+ apiKey: config.apiKey,
835
+ ...config.baseUrl === void 0 ? {} : { baseUrl: config.baseUrl },
836
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
837
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
838
+ });
839
+ }
840
+
841
+ // src/catalog.ts
842
+ var PROVIDER_ID = "dai";
843
+ var DEFAULT_IMAGE_MODEL = "gpt-image-2.5-flare";
844
+ var DEFAULT_VIDEO_MODEL = "veo-3.1-quality";
845
+ var DEFAULT_MUSIC_MODEL = "chirp-v5-5";
846
+ var IMAGE_MODELS = [
847
+ { id: "gpt-image-2.5-flare", family: "openai", generate: true, edit: false },
848
+ { id: "gpt-image-2.5-sunburst", family: "openai", generate: true, edit: false },
849
+ { id: "gpt-image-2-plus", family: "openai", generate: true, edit: false },
850
+ { id: "gpt-image-2", family: "openai", generate: true, edit: false },
851
+ { id: "gpt-image-1.5", family: "openai", generate: true, edit: false },
852
+ { id: "gemini-3-pro-image-preview", family: "gemini", generate: true, edit: true },
853
+ { id: "gemini-3.1-flash-image-preview", family: "gemini", generate: true, edit: true },
854
+ { id: "gemini-3.1-flash-image", family: "gemini", generate: true, edit: true },
855
+ { id: "gemini-3.1-flash-lite-image", family: "gemini", generate: true, edit: true },
856
+ { id: "flux-2-max", family: "flux", generate: true, edit: true },
857
+ { id: "flux-2-pro", family: "flux", generate: true, edit: true },
858
+ { id: "flux-2-flex", family: "flux", generate: true, edit: true },
859
+ { id: "flux-2-klein-9b", family: "flux", generate: true, edit: true },
860
+ { id: "flux-2-klein-4b", family: "flux", generate: true, edit: true },
861
+ { id: "flux-kontext-max", family: "flux", generate: true, edit: true },
862
+ { id: "flux-kontext-pro", family: "flux", generate: true, edit: true },
863
+ { id: "flux1-pro", family: "flux", generate: true, edit: true },
864
+ { id: "flux1-dev", family: "flux", generate: true, edit: true },
865
+ { id: "flux1-schnell", family: "flux", generate: false, edit: true }
866
+ ];
867
+ var VIDEO_MODELS = [
868
+ {
869
+ id: "veo-3.1-quality",
870
+ family: "gemini",
871
+ generate: true,
872
+ imageToVideo: true,
873
+ videoToVideo: false
874
+ },
875
+ {
876
+ id: "veo-3.1-fast",
877
+ family: "gemini",
878
+ generate: true,
879
+ imageToVideo: true,
880
+ videoToVideo: false
881
+ },
882
+ {
883
+ id: "veo-3.1-lite",
884
+ family: "gemini",
885
+ generate: true,
886
+ imageToVideo: true,
887
+ videoToVideo: false
888
+ },
889
+ {
890
+ id: "omni-flash",
891
+ family: "gemini",
892
+ generate: true,
893
+ imageToVideo: true,
894
+ videoToVideo: true
895
+ },
896
+ {
897
+ id: "flux-3-video",
898
+ family: "flux",
899
+ generate: true,
900
+ imageToVideo: true,
901
+ videoToVideo: true
902
+ }
903
+ ];
904
+ var MUSIC_MODELS = [
905
+ "chirp-v5-5",
906
+ "chirp-v5",
907
+ "chirp-v4-5+",
908
+ "chirp-v4-5",
909
+ "chirp-v4-5-all",
910
+ "chirp-v4",
911
+ "chirp-v3-5",
912
+ "chirp-v3-0"
913
+ ];
914
+ var liveImages;
915
+ var liveVideos;
916
+ var liveMusic;
917
+ var imageById = new Map(IMAGE_MODELS.map((model) => [model.id, model]));
918
+ var videoById = new Map(VIDEO_MODELS.map((model) => [model.id, model]));
919
+ var musicIds = new Set(MUSIC_MODELS);
920
+ function currentImageModels() {
921
+ return liveImages ?? IMAGE_MODELS;
922
+ }
923
+ function currentVideoModels() {
924
+ return liveVideos ?? VIDEO_MODELS;
925
+ }
926
+ function currentMusicModels() {
927
+ return liveMusic ?? MUSIC_MODELS;
928
+ }
929
+ function applyDiscoveredMedia(input) {
930
+ liveImages = input.images.length > 0 ? input.images : void 0;
931
+ liveVideos = input.videos.length > 0 ? input.videos : void 0;
932
+ liveMusic = input.music.length > 0 ? input.music : void 0;
933
+ rebuildLookups();
934
+ }
935
+ function rebuildLookups() {
936
+ imageById = new Map(currentImageModels().map((model) => [model.id, model]));
937
+ videoById = new Map(currentVideoModels().map((model) => [model.id, model]));
938
+ musicIds = new Set(currentMusicModels());
939
+ }
940
+ function stripProviderPrefix(model) {
941
+ const trimmed = model?.trim() ?? "";
942
+ if (trimmed.startsWith(`${PROVIDER_ID}/`)) {
943
+ return trimmed.slice(PROVIDER_ID.length + 1);
944
+ }
945
+ return trimmed;
946
+ }
947
+ function resolveImageModel(model) {
948
+ const id = stripProviderPrefix(model) || DEFAULT_IMAGE_MODEL;
949
+ const spec = imageById.get(id);
950
+ if (!spec) {
951
+ throw new Error(`Unknown D.AI image model: ${id}`);
952
+ }
953
+ return spec;
954
+ }
955
+ function resolveVideoModel(model) {
956
+ const id = stripProviderPrefix(model) || DEFAULT_VIDEO_MODEL;
957
+ const spec = videoById.get(id);
958
+ if (!spec) {
959
+ throw new Error(`Unknown D.AI video model: ${id}`);
960
+ }
961
+ return spec;
962
+ }
963
+ function resolveMusicModel(model) {
964
+ const id = stripProviderPrefix(model) || DEFAULT_MUSIC_MODEL;
965
+ if (!musicIds.has(id)) {
966
+ throw new Error(`Unknown D.AI music model: ${id}`);
967
+ }
968
+ return id;
969
+ }
970
+ function assertImageMode(spec, edit) {
971
+ if (edit && !spec.edit) {
972
+ throw new Error(`${spec.id} does not support image edits.`);
973
+ }
974
+ if (!edit && !spec.generate) {
975
+ throw new Error(
976
+ spec.id === "flux1-schnell" ? "flux1-schnell is edits-only. Use a reference image, or generate with flux1-dev." : `${spec.id} does not support text-to-image generation.`
977
+ );
978
+ }
979
+ }
980
+ function imageModelIds() {
981
+ return currentImageModels().map((model) => `${PROVIDER_ID}/${model.id}`);
982
+ }
983
+ function videoModelIds() {
984
+ return currentVideoModels().map((model) => `${PROVIDER_ID}/${model.id}`);
985
+ }
986
+ function musicModelIds() {
987
+ return currentMusicModels().map((model) => `${PROVIDER_ID}/${model}`);
988
+ }
989
+
990
+ // src/config.ts
991
+ import { mkdir, readFile, writeFile } from "fs/promises";
992
+ import { homedir } from "os";
993
+ import { dirname, join } from "path";
994
+
995
+ // src/errors.ts
996
+ var PROVIDER_LEAK = /provider_code|ttapi|upstream job|route_id/iu;
997
+ var DaiPluginError = class extends Error {
998
+ name = "DaiPluginError";
999
+ };
1000
+ function missingApiKeyError() {
1001
+ return new DaiPluginError(
1002
+ "Set DAI_API_KEY, write ~/.pi/agent/dai.json, or run /dai key."
1003
+ );
1004
+ }
1005
+ function toUserError(error) {
1006
+ if (error instanceof DaiPluginError) {
1007
+ return error;
1008
+ }
1009
+ if (error instanceof RequestAbortedError || error instanceof RequestTimeoutError) {
1010
+ return new DaiPluginError("Generation cancelled or timed out.");
1011
+ }
1012
+ if (error instanceof WaitTimeoutError) {
1013
+ return new DaiPluginError("Generation cancelled or timed out.");
1014
+ }
1015
+ if (error instanceof ApiError) {
1016
+ if (error.status === 401 || error.status === 403) {
1017
+ return new DaiPluginError("Invalid D.AI API key.");
1018
+ }
1019
+ if (error.status === 402 || /余额不足|insufficient_quota/iu.test(error.message)) {
1020
+ return new DaiPluginError("D.AI \u4F59\u989D\u4E0D\u8DB3\u3002");
1021
+ }
1022
+ if (error.status >= 400 && error.status < 500) {
1023
+ return new DaiPluginError(sanitize(publicMessage(error)));
1024
+ }
1025
+ return new DaiPluginError("Generation failed.");
1026
+ }
1027
+ if (error instanceof Error) {
1028
+ return new DaiPluginError(sanitize(error.message));
1029
+ }
1030
+ return new DaiPluginError("Generation failed.");
1031
+ }
1032
+ function publicMessage(error) {
1033
+ const rawMessage = typeof error.raw?.message === "string" ? error.raw.message : error.message.replace(/ \(HTTP \d+\)$/u, "");
1034
+ return rawMessage.trim() || "Generation failed.";
1035
+ }
1036
+ function sanitize(message) {
1037
+ const cleaned = message.replace(PROVIDER_LEAK, "").replace(/\s+/gu, " ").trim();
1038
+ return cleaned.length > 0 ? cleaned : "Generation failed.";
1039
+ }
1040
+
1041
+ // src/config.ts
1042
+ function configPath() {
1043
+ return join(homedir(), ".pi", "agent", "dai.json");
1044
+ }
1045
+ async function loadFileConfig(path = configPath()) {
1046
+ try {
1047
+ const raw = JSON.parse(await readFile(path, "utf8"));
1048
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1049
+ return {};
1050
+ }
1051
+ const record = raw;
1052
+ return {
1053
+ ...typeof record.apiKey === "string" ? { apiKey: record.apiKey } : {},
1054
+ ...typeof record.baseUrl === "string" ? { baseUrl: record.baseUrl } : {}
1055
+ };
1056
+ } catch (error) {
1057
+ if (isMissingFile(error)) {
1058
+ return {};
1059
+ }
1060
+ throw error;
1061
+ }
1062
+ }
1063
+ async function saveFileConfig(config, path = configPath()) {
1064
+ await mkdir(dirname(path), { recursive: true });
1065
+ const current = await loadFileConfig(path);
1066
+ const next = {
1067
+ ...current,
1068
+ ...config.apiKey === void 0 ? {} : { apiKey: config.apiKey.trim() },
1069
+ ...config.baseUrl === void 0 ? {} : { baseUrl: config.baseUrl.trim() }
1070
+ };
1071
+ await writeFile(`${path}`, `${JSON.stringify(next, null, 2)}
1072
+ `, { mode: 384 });
1073
+ }
1074
+ async function resolveConfig(env = process.env, path = configPath()) {
1075
+ const file = await loadFileConfig(path);
1076
+ const apiKey = firstNonEmpty(file.apiKey, env.DAI_API_KEY);
1077
+ if (!apiKey) {
1078
+ throw missingApiKeyError();
1079
+ }
1080
+ const baseUrl = firstNonEmpty(file.baseUrl, env.DAI_BASE_URL);
1081
+ return baseUrl === void 0 ? { apiKey } : { apiKey, baseUrl };
1082
+ }
1083
+ function hasApiKey(config, env = process.env) {
1084
+ return Boolean(firstNonEmpty(config.apiKey, env.DAI_API_KEY));
1085
+ }
1086
+ function firstNonEmpty(...values) {
1087
+ for (const value of values) {
1088
+ const trimmed = value?.trim();
1089
+ if (trimmed) {
1090
+ return trimmed;
1091
+ }
1092
+ }
1093
+ return void 0;
1094
+ }
1095
+ function isMissingFile(error) {
1096
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1097
+ }
1098
+
1099
+ // src/origin.ts
1100
+ var DEFAULT_BASE_URL2 = "https://dai.itbug.shop";
1101
+ function normalizeOrigin(baseUrl) {
1102
+ return baseUrl.replace(/\/+$/u, "").replace(/\/v1$/u, "");
1103
+ }
1104
+
1105
+ // src/discover.ts
1106
+ async function discoverDaiCatalog(options = {}) {
1107
+ const origin = normalizeOrigin(options.origin ?? DEFAULT_BASE_URL2);
1108
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1109
+ const docs = await fetchJson(
1110
+ fetchImpl,
1111
+ `${origin}/api/system/public/capability-docs`,
1112
+ options.signal
1113
+ );
1114
+ const catalog = parseCapabilityDocs(docs);
1115
+ if (options.apiKey) {
1116
+ const listed = await fetchJson(
1117
+ fetchImpl,
1118
+ `${origin}/v1/models`,
1119
+ options.signal,
1120
+ options.apiKey
1121
+ );
1122
+ const allowed = new Set(
1123
+ (listed.data ?? []).map((model) => model.id?.trim()).filter((id) => Boolean(id))
1124
+ );
1125
+ if (allowed.size > 0) {
1126
+ catalog.chat = catalog.chat.filter((model) => allowed.has(model.id));
1127
+ }
1128
+ }
1129
+ applyDiscoveredMedia({
1130
+ images: catalog.images,
1131
+ videos: catalog.videos,
1132
+ music: catalog.music
1133
+ });
1134
+ return catalog;
1135
+ }
1136
+ function parseCapabilityDocs(docs) {
1137
+ const chat = [];
1138
+ const images = [];
1139
+ const videos = [];
1140
+ const music = [];
1141
+ const seenChat = /* @__PURE__ */ new Set();
1142
+ const seenImage = /* @__PURE__ */ new Set();
1143
+ const seenVideo = /* @__PURE__ */ new Set();
1144
+ const seenMusic = /* @__PURE__ */ new Set();
1145
+ for (const capability of docs.data ?? []) {
1146
+ const code = capability.capability_code?.trim() ?? "";
1147
+ for (const model of capability.models ?? []) {
1148
+ const id = model.model_code?.trim();
1149
+ if (!id) {
1150
+ continue;
1151
+ }
1152
+ if (code === "openai_chat_completions" && model.token_rate) {
1153
+ if (seenChat.has(id)) {
1154
+ continue;
1155
+ }
1156
+ seenChat.add(id);
1157
+ chat.push(toChatModel(id, model));
1158
+ continue;
1159
+ }
1160
+ if (code === "openai_image" || code === "gemini_image" || code === "flux_image") {
1161
+ if (seenImage.has(id)) {
1162
+ continue;
1163
+ }
1164
+ seenImage.add(id);
1165
+ images.push(toImageSpec(code, id));
1166
+ continue;
1167
+ }
1168
+ if (code === "gemini_video" || code === "flux_video") {
1169
+ if (seenVideo.has(id)) {
1170
+ continue;
1171
+ }
1172
+ seenVideo.add(id);
1173
+ videos.push(toVideoSpec(code, id));
1174
+ continue;
1175
+ }
1176
+ if (code === "suno_music") {
1177
+ if (seenMusic.has(id)) {
1178
+ continue;
1179
+ }
1180
+ seenMusic.add(id);
1181
+ music.push(id);
1182
+ }
1183
+ }
1184
+ }
1185
+ return { chat, images, videos, music };
1186
+ }
1187
+ function toChatModel(id, model) {
1188
+ const path = model.integration_docs?.path ?? "";
1189
+ const responses = path.includes("/responses");
1190
+ const rate = model.token_rate;
1191
+ return {
1192
+ id,
1193
+ name: model.display_name?.trim() || id,
1194
+ api: responses ? "openai-responses" : "openai-completions",
1195
+ reasoning: /gpt-5|o1|o3|o4|reason/iu.test(id),
1196
+ input: ["text"],
1197
+ cost: {
1198
+ input: rate?.input_usd_per_1m_tokens ?? 0,
1199
+ output: rate?.output_usd_per_1m_tokens ?? 0,
1200
+ cacheRead: 0,
1201
+ cacheWrite: 0
1202
+ },
1203
+ contextWindow: Math.max(1, rate?.context_window_tokens ?? 128e3),
1204
+ maxTokens: Math.max(1, rate?.prepay_completion_token_limit ?? 16384)
1205
+ };
1206
+ }
1207
+ function toImageSpec(capability, id) {
1208
+ if (capability === "openai_image") {
1209
+ return { id, family: "openai", generate: true, edit: false };
1210
+ }
1211
+ if (capability === "gemini_image") {
1212
+ return { id, family: "gemini", generate: true, edit: true };
1213
+ }
1214
+ return {
1215
+ id,
1216
+ family: "flux",
1217
+ generate: id !== "flux1-schnell",
1218
+ edit: true
1219
+ };
1220
+ }
1221
+ function toVideoSpec(capability, id) {
1222
+ if (capability === "flux_video") {
1223
+ return { id, family: "flux", generate: true, imageToVideo: true, videoToVideo: true };
1224
+ }
1225
+ return {
1226
+ id,
1227
+ family: "gemini",
1228
+ generate: true,
1229
+ imageToVideo: true,
1230
+ videoToVideo: id === "omni-flash"
1231
+ };
1232
+ }
1233
+ async function fetchJson(fetchImpl, url, signal, apiKey) {
1234
+ const headers = new Headers();
1235
+ if (apiKey) {
1236
+ headers.set("Authorization", `Bearer ${apiKey}`);
1237
+ }
1238
+ const response = await fetchImpl(url, {
1239
+ headers,
1240
+ ...signal === void 0 ? {} : { signal }
1241
+ });
1242
+ if (!response.ok) {
1243
+ throw new Error(`D.AI catalog request failed (HTTP ${response.status}).`);
1244
+ }
1245
+ return await response.json();
1246
+ }
1247
+
1248
+ // src/assets.ts
1249
+ import { readFile as readFile2 } from "fs/promises";
1250
+ import { basename, extname } from "path";
1251
+ var HTTP_URL = /^https?:\/\//iu;
1252
+ var DATA_URL = /^data:/iu;
1253
+ var FILE_URL = /^file:/iu;
1254
+ function collectReferences(request) {
1255
+ const images = [
1256
+ ...request.image ? [request.image] : [],
1257
+ ...request.images ?? []
1258
+ ].filter((value) => value.trim().length > 0);
1259
+ const videos = [
1260
+ ...request.video ? [request.video] : [],
1261
+ ...request.videos ?? []
1262
+ ].filter((value) => value.trim().length > 0);
1263
+ return { images, videos };
1264
+ }
1265
+ function optionalSignal(signal) {
1266
+ return signal === void 0 ? {} : { signal };
1267
+ }
1268
+ async function toRemoteMedia(value, options = {}) {
1269
+ const trimmed = value.trim();
1270
+ if (HTTP_URL.test(trimmed) || DATA_URL.test(trimmed)) {
1271
+ return trimmed;
1272
+ }
1273
+ if (FILE_URL.test(trimmed)) {
1274
+ return dataUrlFromFile(fileURLToPathSafe(trimmed), options);
1275
+ }
1276
+ return dataUrlFromFile(trimmed, options);
1277
+ }
1278
+ function absoluteAssetUrl(baseUrl, asset) {
1279
+ const trimmed = asset.trim();
1280
+ if (HTTP_URL.test(trimmed) || DATA_URL.test(trimmed)) {
1281
+ return trimmed;
1282
+ }
1283
+ if (trimmed.startsWith("/")) {
1284
+ return new URL(trimmed, ensureTrailingSlash(baseUrl)).toString();
1285
+ }
1286
+ return trimmed;
1287
+ }
1288
+ async function fetchMediaBuffer(url, options = {}) {
1289
+ const response = await fetch(url, optionalSignal(options.signal));
1290
+ if (!response.ok) {
1291
+ throw new Error("Generated media could not be downloaded.");
1292
+ }
1293
+ const mimeType = response.headers.get("content-type")?.split(";")[0]?.trim() || guessMime(url);
1294
+ const buffer = Buffer.from(await response.arrayBuffer());
1295
+ if (buffer.byteLength === 0) {
1296
+ throw new Error("Generated media was empty.");
1297
+ }
1298
+ return {
1299
+ buffer,
1300
+ mimeType,
1301
+ fileName: options.fileName ?? filenameFromUrl(url, mimeType)
1302
+ };
1303
+ }
1304
+ async function dataUrlFromFile(filePath, options) {
1305
+ if (options.signal?.aborted) {
1306
+ throw options.signal.reason instanceof Error ? options.signal.reason : new Error("Generation cancelled or timed out.");
1307
+ }
1308
+ const bytes = await readFile2(filePath);
1309
+ const mime = guessMime(filePath);
1310
+ return `data:${mime};base64,${bytes.toString("base64")}`;
1311
+ }
1312
+ function fileURLToPathSafe(value) {
1313
+ return new URL(value).pathname;
1314
+ }
1315
+ function ensureTrailingSlash(baseUrl) {
1316
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1317
+ }
1318
+ function guessMime(path) {
1319
+ switch (extname(path).toLowerCase()) {
1320
+ case ".png":
1321
+ return "image/png";
1322
+ case ".jpg":
1323
+ case ".jpeg":
1324
+ return "image/jpeg";
1325
+ case ".webp":
1326
+ return "image/webp";
1327
+ case ".gif":
1328
+ return "image/gif";
1329
+ case ".mp4":
1330
+ return "video/mp4";
1331
+ case ".webm":
1332
+ return "video/webm";
1333
+ case ".mp3":
1334
+ return "audio/mpeg";
1335
+ case ".wav":
1336
+ return "audio/wav";
1337
+ default:
1338
+ return "application/octet-stream";
1339
+ }
1340
+ }
1341
+ function filenameFromUrl(url, mimeType) {
1342
+ try {
1343
+ const name = basename(new URL(url).pathname);
1344
+ if (name) {
1345
+ return name;
1346
+ }
1347
+ } catch {
1348
+ }
1349
+ const ext = mimeType.split("/")[1] ?? "bin";
1350
+ return `dai-media.${ext}`;
1351
+ }
1352
+
1353
+ // src/image.ts
1354
+ var FLUX_WAIT_MS = 3 * 6e4;
1355
+ async function generateDaiImage(client, request) {
1356
+ try {
1357
+ const spec = resolveImageModel(request.model);
1358
+ const refs = collectReferences(request);
1359
+ const edit = refs.images.length > 0;
1360
+ assertImageMode(spec, edit);
1361
+ const remoteImages = await Promise.all(
1362
+ refs.images.map((image) => toRemoteMedia(image, optionalSignal(request.signal)))
1363
+ );
1364
+ if (spec.family === "openai") {
1365
+ const result = await client.images.openai.generate(
1366
+ {
1367
+ prompt: request.prompt,
1368
+ model: spec.id,
1369
+ ...request.size === void 0 ? {} : { size: request.size },
1370
+ ...request.quality === void 0 ? {} : { quality: request.quality },
1371
+ ...request.outputFormat === void 0 ? {} : { outputFormat: request.outputFormat },
1372
+ ...request.background === void 0 ? {} : { background: request.background },
1373
+ ...request.count === void 0 ? {} : { n: request.count }
1374
+ },
1375
+ optionalSignal(request.signal)
1376
+ );
1377
+ return { images: await buffersFromImageResult(client, result, request) };
1378
+ }
1379
+ if (spec.family === "gemini") {
1380
+ const result = await client.images.gemini.generate(
1381
+ {
1382
+ model: spec.id,
1383
+ prompt: request.prompt,
1384
+ ...request.aspectRatio === void 0 ? {} : { aspectRatio: request.aspectRatio },
1385
+ referenceImages: remoteImages
1386
+ },
1387
+ optionalSignal(request.signal)
1388
+ );
1389
+ return { images: await buffersFromImageResult(client, result, request) };
1390
+ }
1391
+ const job = edit ? await client.images.flux.edits(
1392
+ {
1393
+ prompt: request.prompt,
1394
+ model: spec.id,
1395
+ image: remoteImages.length === 1 ? remoteImages[0] : remoteImages,
1396
+ ...request.size === void 0 ? {} : { size: request.size }
1397
+ },
1398
+ optionalSignal(request.signal)
1399
+ ) : await client.images.flux.generate(
1400
+ {
1401
+ prompt: request.prompt,
1402
+ model: spec.id,
1403
+ ...request.size === void 0 ? {} : { size: request.size }
1404
+ },
1405
+ optionalSignal(request.signal)
1406
+ );
1407
+ const waited = await client.jobs.wait(job, {
1408
+ pollIntervalMs: 2e3,
1409
+ maxWaitMs: FLUX_WAIT_MS,
1410
+ ...optionalSignal(request.signal)
1411
+ });
1412
+ return { images: await buffersFromJob(client, waited, request, "image/png") };
1413
+ } catch (error) {
1414
+ throw toUserError(error);
1415
+ }
1416
+ }
1417
+ async function buffersFromImageResult(client, result, request) {
1418
+ const images = [];
1419
+ for (const [index, image] of result.images.entries()) {
1420
+ if (image.b64Json) {
1421
+ images.push({
1422
+ buffer: Buffer.from(image.b64Json, "base64"),
1423
+ mimeType: mimeFromFormat(request.outputFormat),
1424
+ fileName: request.filename ?? `dai-image-${index + 1}.png`
1425
+ });
1426
+ continue;
1427
+ }
1428
+ if (!image.url) {
1429
+ continue;
1430
+ }
1431
+ images.push(
1432
+ await fetchMediaBuffer(absoluteAssetUrl(client.baseUrl, image.url), {
1433
+ ...optionalSignal(request.signal),
1434
+ ...request.filename === void 0 ? {} : { fileName: request.filename }
1435
+ })
1436
+ );
1437
+ }
1438
+ if (images.length === 0) {
1439
+ throw new Error("D.AI did not return an image.");
1440
+ }
1441
+ return images;
1442
+ }
1443
+ async function buffersFromJob(client, result, request, fallbackMime) {
1444
+ if (result.state === "failed") {
1445
+ throw new Error(result.errorMessage ?? "Generation failed.");
1446
+ }
1447
+ const images = [];
1448
+ for (const [index, asset] of result.assets.entries()) {
1449
+ images.push(
1450
+ await fetchMediaBuffer(absoluteAssetUrl(client.baseUrl, asset), {
1451
+ ...optionalSignal(request.signal),
1452
+ fileName: request.filename ?? `dai-image-${index + 1}.${extensionFor(fallbackMime)}`
1453
+ })
1454
+ );
1455
+ }
1456
+ if (images.length === 0) {
1457
+ throw new Error("D.AI did not return an image.");
1458
+ }
1459
+ return images;
1460
+ }
1461
+ function mimeFromFormat(format) {
1462
+ switch (format) {
1463
+ case "jpeg":
1464
+ return "image/jpeg";
1465
+ case "webp":
1466
+ return "image/webp";
1467
+ default:
1468
+ return "image/png";
1469
+ }
1470
+ }
1471
+ function extensionFor(mimeType) {
1472
+ return mimeType.split("/")[1] ?? "bin";
1473
+ }
1474
+
1475
+ // src/music.ts
1476
+ var MUSIC_WAIT_MS = 5 * 6e4;
1477
+ async function generateDaiMusic(client, request) {
1478
+ try {
1479
+ const mv = resolveMusicModel(request.model);
1480
+ const lyrics = request.lyrics?.trim();
1481
+ const custom = Boolean(lyrics);
1482
+ const job = await client.audio.suno.music(
1483
+ {
1484
+ custom,
1485
+ instrumental: request.instrumental ?? !custom,
1486
+ mv,
1487
+ ...custom && lyrics !== void 0 ? { prompt: lyrics } : { gptDescriptionPrompt: request.prompt }
1488
+ },
1489
+ optionalSignal(request.signal)
1490
+ );
1491
+ const waited = await client.jobs.wait(job, {
1492
+ pollIntervalMs: 2e3,
1493
+ maxWaitMs: MUSIC_WAIT_MS,
1494
+ ...optionalSignal(request.signal)
1495
+ });
1496
+ return { tracks: remoteMediaFromJob(client, waited, request, "audio/mpeg") };
1497
+ } catch (error) {
1498
+ throw toUserError(error);
1499
+ }
1500
+ }
1501
+ function remoteMediaFromJob(client, result, request, mimeType) {
1502
+ if (result.state === "failed") {
1503
+ throw new Error(result.errorMessage ?? "Generation failed.");
1504
+ }
1505
+ const tracks = result.assets.map((asset) => ({
1506
+ url: absoluteAssetUrl(client.baseUrl, asset),
1507
+ mimeType,
1508
+ ...request.filename === void 0 ? {} : { fileName: request.filename }
1509
+ }));
1510
+ if (tracks.length === 0) {
1511
+ throw new Error("D.AI did not return audio.");
1512
+ }
1513
+ return tracks;
1514
+ }
1515
+
1516
+ // src/provider.ts
1517
+ import { createProvider } from "@earendil-works/pi-ai";
1518
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
1519
+ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
1520
+ function registerDaiProvider(pi) {
1521
+ pi.registerProvider(
1522
+ createProvider({
1523
+ id: "dai",
1524
+ name: "D.AI",
1525
+ baseUrl: `${DEFAULT_BASE_URL2}/v1`,
1526
+ auth: {
1527
+ apiKey: {
1528
+ name: "D.AI API key",
1529
+ async login(interaction) {
1530
+ const key = await interaction.prompt({
1531
+ type: "secret",
1532
+ message: "D.AI API key (ddsk_...)",
1533
+ placeholder: "ddsk_..."
1534
+ });
1535
+ const trimmed = key.trim();
1536
+ await saveFileConfig({ apiKey: trimmed });
1537
+ process.env.DAI_API_KEY = trimmed;
1538
+ return { type: "api_key", key: trimmed };
1539
+ },
1540
+ async resolve({
1541
+ credential,
1542
+ ctx
1543
+ }) {
1544
+ const file = await loadFileConfig();
1545
+ const key = credential?.key?.trim() || await ctx.env("DAI_API_KEY") || file.apiKey?.trim();
1546
+ if (!key) {
1547
+ return void 0;
1548
+ }
1549
+ const origin = normalizeOrigin(file.baseUrl ?? DEFAULT_BASE_URL2);
1550
+ return {
1551
+ auth: { apiKey: key, baseUrl: `${origin}/v1` },
1552
+ source: credential?.key ? "stored D.AI API key" : "DAI_API_KEY"
1553
+ };
1554
+ }
1555
+ }
1556
+ },
1557
+ models: [],
1558
+ async fetchModels(context) {
1559
+ if (!context.allowNetwork) {
1560
+ return context.stored?.models ?? [];
1561
+ }
1562
+ const file = await loadFileConfig();
1563
+ const key = (context.credential && "key" in context.credential ? context.credential.key : void 0) || file.apiKey;
1564
+ const origin = normalizeOrigin(file.baseUrl ?? DEFAULT_BASE_URL2);
1565
+ const catalog = await discoverDaiCatalog({
1566
+ origin,
1567
+ ...key === void 0 ? {} : { apiKey: key },
1568
+ signal: context.signal
1569
+ });
1570
+ return toPiModels(catalog.chat, `${origin}/v1`);
1571
+ },
1572
+ api: {
1573
+ "openai-completions": openAICompletionsApi(),
1574
+ "openai-responses": openAIResponsesApi()
1575
+ }
1576
+ })
1577
+ );
1578
+ }
1579
+ function toPiModels(models, baseUrl) {
1580
+ return models.map((model) => ({
1581
+ id: model.id,
1582
+ name: model.name,
1583
+ api: model.api,
1584
+ provider: "dai",
1585
+ baseUrl,
1586
+ reasoning: model.reasoning,
1587
+ input: model.input,
1588
+ cost: model.cost,
1589
+ contextWindow: model.contextWindow,
1590
+ maxTokens: model.maxTokens
1591
+ }));
1592
+ }
1593
+
1594
+ // src/video.ts
1595
+ var VIDEO_WAIT_MS = 12 * 6e4;
1596
+ async function generateDaiVideo(client, request) {
1597
+ try {
1598
+ const spec = resolveVideoModel(request.model);
1599
+ const refs = collectReferences(request);
1600
+ if (refs.images.length > 0 && refs.videos.length > 0) {
1601
+ throw new Error("Use either reference images or a reference video, not both.");
1602
+ }
1603
+ if (refs.videos.length > 0 && !spec.videoToVideo) {
1604
+ throw new Error(`${spec.id} does not support video-to-video.`);
1605
+ }
1606
+ if (refs.images.length > 0 && !spec.imageToVideo) {
1607
+ throw new Error(`${spec.id} does not support image-to-video.`);
1608
+ }
1609
+ const remoteImages = await Promise.all(
1610
+ refs.images.map((image) => toRemoteMedia(image, optionalSignal(request.signal)))
1611
+ );
1612
+ const remoteVideos = await Promise.all(
1613
+ refs.videos.map((video) => toRemoteMedia(video, optionalSignal(request.signal)))
1614
+ );
1615
+ const job = spec.family === "gemini" ? await client.videos.gemini.generate(
1616
+ {
1617
+ prompt: request.prompt,
1618
+ model: spec.id,
1619
+ aspectRatio: request.aspectRatio === "9:16" ? "9:16" : "16:9",
1620
+ ...request.resolution === void 0 ? {} : { resolution: mapGeminiResolution(request.resolution) },
1621
+ ...request.durationSeconds === void 0 ? {} : { duration: request.durationSeconds },
1622
+ ...remoteImages.length > 0 ? { generationType: "frame", referenceImages: remoteImages } : {},
1623
+ ...remoteVideos.length > 0 ? { referenceVideos: remoteVideos } : {}
1624
+ },
1625
+ optionalSignal(request.signal)
1626
+ ) : await client.videos.flux.generate(
1627
+ {
1628
+ prompt: request.prompt,
1629
+ model: spec.id,
1630
+ mode: fluxMode(remoteImages, remoteVideos),
1631
+ ...request.aspectRatio === void 0 ? {} : { aspectRatio: request.aspectRatio },
1632
+ ...request.durationSeconds === void 0 ? {} : { duration: clampFluxDuration(request.durationSeconds) },
1633
+ ...request.resolution === void 0 ? {} : { resolution: mapFluxResolution(request.resolution) },
1634
+ ...request.audio === void 0 ? {} : { generateAudio: request.audio },
1635
+ ...remoteImages.length > 0 ? { keyframes: remoteImages } : {},
1636
+ ...remoteVideos[0] === void 0 ? {} : { startVideo: remoteVideos[0] }
1637
+ },
1638
+ optionalSignal(request.signal)
1639
+ );
1640
+ const waited = await client.jobs.wait(job, {
1641
+ pollIntervalMs: 3e3,
1642
+ maxWaitMs: VIDEO_WAIT_MS,
1643
+ ...optionalSignal(request.signal)
1644
+ });
1645
+ return { videos: remoteMediaFromJob2(client, waited, request, "video/mp4") };
1646
+ } catch (error) {
1647
+ throw toUserError(error);
1648
+ }
1649
+ }
1650
+ function fluxMode(images, videos) {
1651
+ if (videos.length > 0) {
1652
+ return "v2v";
1653
+ }
1654
+ if (images.length > 0) {
1655
+ return "i2v";
1656
+ }
1657
+ return "t2v";
1658
+ }
1659
+ function clampFluxDuration(seconds) {
1660
+ return Math.min(20, Math.max(5, Math.round(seconds)));
1661
+ }
1662
+ function mapGeminiResolution(value) {
1663
+ const normalized = value.trim().toLowerCase();
1664
+ if (normalized === "1080p" || normalized === "1080" || normalized === "fhd") {
1665
+ return "1080p";
1666
+ }
1667
+ return "720p";
1668
+ }
1669
+ function mapFluxResolution(value) {
1670
+ const normalized = value.trim().toLowerCase();
1671
+ if (normalized === "1080p" || normalized === "1080" || normalized === "fhd" || normalized === "4k") {
1672
+ return "fhd";
1673
+ }
1674
+ return "hd";
1675
+ }
1676
+ function remoteMediaFromJob2(client, result, request, mimeType) {
1677
+ if (result.state === "failed") {
1678
+ throw new Error(result.errorMessage ?? "Generation failed.");
1679
+ }
1680
+ const videos = result.assets.map((asset) => ({
1681
+ url: absoluteAssetUrl(client.baseUrl, asset),
1682
+ mimeType,
1683
+ ...request.filename === void 0 ? {} : { fileName: request.filename }
1684
+ }));
1685
+ if (videos.length === 0) {
1686
+ throw new Error("D.AI did not return a video.");
1687
+ }
1688
+ return videos;
1689
+ }
1690
+
1691
+ // src/index.ts
1692
+ var optionalString = Type.Optional(Type.String());
1693
+ var optionalStringArray = Type.Optional(Type.Array(Type.String()));
1694
+ async function index_default(pi) {
1695
+ registerDaiProvider(pi);
1696
+ try {
1697
+ const config = await resolveConfig();
1698
+ await discoverDaiCatalog({
1699
+ ...config.baseUrl === void 0 ? {} : { origin: config.baseUrl },
1700
+ apiKey: config.apiKey
1701
+ });
1702
+ } catch {
1703
+ try {
1704
+ await discoverDaiCatalog();
1705
+ } catch {
1706
+ }
1707
+ }
1708
+ pi.registerTool({
1709
+ name: "dai_generate_image",
1710
+ label: "D.AI Image",
1711
+ description: "Generate or edit an image through D.AI. Default model is dai/gpt-image-2.5-flare. GPT Image models are text-to-image only. Use Gemini Image or Flux for edits with a reference image. flux1-schnell is edits-only.",
1712
+ promptSnippet: "Generate or edit images via D.AI (GPT Image 2.5, Gemini, Flux)",
1713
+ promptGuidelines: [
1714
+ "Use dai_generate_image when the user asks to generate or edit an image with D.AI.",
1715
+ "Pass model as a public id such as gpt-image-2.5-flare or dai/flux-2-max."
1716
+ ],
1717
+ parameters: Type.Object({
1718
+ prompt: Type.String({ description: "Image prompt or edit instruction." }),
1719
+ model: optionalString,
1720
+ image: optionalString,
1721
+ images: optionalStringArray,
1722
+ size: optionalString,
1723
+ aspectRatio: optionalString,
1724
+ quality: optionalString,
1725
+ count: Type.Optional(Type.Number())
1726
+ }),
1727
+ async execute(_id, params, signal, onUpdate) {
1728
+ try {
1729
+ onUpdate?.({ content: [{ type: "text", text: "Generating image with D.AI..." }] });
1730
+ const client = createMediaClient(await resolveConfig());
1731
+ const result = await generateDaiImage(client, { ...params, ...optionalSignal(signal) });
1732
+ const first = result.images[0];
1733
+ if (!first) {
1734
+ throw new Error("D.AI did not return an image.");
1735
+ }
1736
+ const model = params.model?.trim() || DEFAULT_IMAGE_MODEL;
1737
+ return {
1738
+ content: [
1739
+ {
1740
+ type: "text",
1741
+ text: `Generated with ${model}. ${result.images.length} image(s).`
1742
+ },
1743
+ {
1744
+ type: "image",
1745
+ source: {
1746
+ type: "base64",
1747
+ mediaType: first.mimeType,
1748
+ data: first.buffer.toString("base64")
1749
+ }
1750
+ }
1751
+ ],
1752
+ details: { model, count: result.images.length, fileName: first.fileName }
1753
+ };
1754
+ } catch (error) {
1755
+ const message = toUserError(error).message;
1756
+ return { content: [{ type: "text", text: message }], isError: true, details: { error: message } };
1757
+ }
1758
+ }
1759
+ });
1760
+ pi.registerTool({
1761
+ name: "dai_generate_video",
1762
+ label: "D.AI Video",
1763
+ description: "Generate video through D.AI. Default model is dai/veo-3.1-quality. Optional reference images become image-to-video; a reference video is video-to-video when the model supports it.",
1764
+ promptSnippet: "Generate video via D.AI (Veo 3.1, omni-flash, Flux 3)",
1765
+ promptGuidelines: [
1766
+ "Use dai_generate_video when the user asks D.AI to generate a video."
1767
+ ],
1768
+ parameters: Type.Object({
1769
+ prompt: Type.String({ description: "Video prompt." }),
1770
+ model: optionalString,
1771
+ image: optionalString,
1772
+ images: optionalStringArray,
1773
+ video: optionalString,
1774
+ videos: optionalStringArray,
1775
+ aspectRatio: optionalString,
1776
+ resolution: optionalString,
1777
+ durationSeconds: Type.Optional(Type.Number()),
1778
+ audio: Type.Optional(Type.Boolean())
1779
+ }),
1780
+ async execute(_id, params, signal, onUpdate) {
1781
+ try {
1782
+ onUpdate?.({ content: [{ type: "text", text: "Submitting D.AI video job..." }] });
1783
+ const client = createMediaClient(await resolveConfig());
1784
+ const result = await generateDaiVideo(client, { ...params, ...optionalSignal(signal) });
1785
+ const urls = result.videos.map((item) => item.url).join("\n");
1786
+ return {
1787
+ content: [{ type: "text", text: `Video ready:
1788
+ ${urls}` }],
1789
+ details: { model: params.model?.trim() || DEFAULT_VIDEO_MODEL, urls: result.videos.map((item) => item.url) }
1790
+ };
1791
+ } catch (error) {
1792
+ const message = toUserError(error).message;
1793
+ return { content: [{ type: "text", text: message }], isError: true, details: { error: message } };
1794
+ }
1795
+ }
1796
+ });
1797
+ pi.registerTool({
1798
+ name: "dai_generate_music",
1799
+ label: "D.AI Music",
1800
+ description: "Generate music through D.AI Suno. Default model is dai/chirp-v5-5. Use lyrics for custom mode; otherwise the prompt is a style description.",
1801
+ promptSnippet: "Generate music via D.AI Suno",
1802
+ promptGuidelines: [
1803
+ "Use dai_generate_music when the user asks D.AI to generate a song or instrumental."
1804
+ ],
1805
+ parameters: Type.Object({
1806
+ prompt: Type.String({ description: "Style description, or ignored when lyrics are set." }),
1807
+ model: optionalString,
1808
+ lyrics: optionalString,
1809
+ instrumental: Type.Optional(Type.Boolean())
1810
+ }),
1811
+ async execute(_id, params, signal, onUpdate) {
1812
+ try {
1813
+ onUpdate?.({ content: [{ type: "text", text: "Submitting D.AI music job..." }] });
1814
+ const client = createMediaClient(await resolveConfig());
1815
+ const result = await generateDaiMusic(client, { ...params, ...optionalSignal(signal) });
1816
+ const urls = result.tracks.map((item) => item.url).join("\n");
1817
+ return {
1818
+ content: [{ type: "text", text: `Track ready:
1819
+ ${urls}` }],
1820
+ details: { model: params.model?.trim() || DEFAULT_MUSIC_MODEL, urls: result.tracks.map((item) => item.url) }
1821
+ };
1822
+ } catch (error) {
1823
+ const message = toUserError(error).message;
1824
+ return { content: [{ type: "text", text: message }], isError: true, details: { error: message } };
1825
+ }
1826
+ }
1827
+ });
1828
+ pi.registerCommand("dai", {
1829
+ description: "D.AI media status, or `/dai key` to save an API key",
1830
+ handler: async (args, ctx) => {
1831
+ const trimmed = args.trim();
1832
+ if (trimmed === "key" || trimmed.startsWith("key ")) {
1833
+ const inline = trimmed.slice(3).trim();
1834
+ const apiKey = inline || (ctx.hasUI ? await ctx.ui.input("D.AI API key", "ddsk_...") : void 0);
1835
+ if (!apiKey?.trim()) {
1836
+ ctx.ui.notify("No API key provided.", "warning");
1837
+ return;
1838
+ }
1839
+ await saveFileConfig({ apiKey: apiKey.trim() });
1840
+ ctx.ui.notify("Saved D.AI API key to ~/.pi/agent/dai.json", "info");
1841
+ return;
1842
+ }
1843
+ const file = await loadFileConfig();
1844
+ const ready = hasApiKey(file);
1845
+ const models = [
1846
+ `image ${DEFAULT_IMAGE_MODEL}`,
1847
+ `video ${DEFAULT_VIDEO_MODEL}`,
1848
+ `music ${DEFAULT_MUSIC_MODEL}`
1849
+ ].join(" \xB7 ");
1850
+ ctx.ui.notify(
1851
+ ready ? `D.AI ready. Defaults: ${models}. Catalog ${imageModelIds().length} images, ${videoModelIds().length} videos, ${musicModelIds().length} music.` : "D.AI key missing. Run /login and choose D.AI, or /dai key / DAI_API_KEY.",
1852
+ ready ? "info" : "warning"
1853
+ );
1854
+ }
1855
+ });
1856
+ }
1857
+ export {
1858
+ index_default as default
1859
+ };