@warlock.js/ai-openai 4.5.0 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to `@warlock.js/ai-openai` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 4.6.0
8
+
9
+ ### Added
10
+
11
+ - **`openai.image({ name })`** — image generation for the `gpt-image-*` (token-metered) and `dall-e-*` (per-image) families, for use with `ai.image()`. A non-image model id is rejected at construction.
12
+ - **PDF + audio input.** `pdf` and `audio` content parts now map to OpenAI `file` (base64 `file_data`) and `input_audio` (`wav` / `mp3`) parts — opt in with `model({ pdf: true })` / `{ audio: true }`. A remote-URL pdf/audio source raises a typed `InvalidRequestError` up front.
13
+ - **`openai.speech({ name })`** — text-to-speech for the `tts-1` / `tts-1-hd` / `gpt-4o-mini-tts` families (`audio.speech.create`), for use with `ai.speech()`.
14
+ - **`openai.transcribe({ name })`** — speech-to-text for the `whisper-1` / `gpt-4o-transcribe` families (`audio.transcriptions.create`), for use with `ai.transcribe()`. `whisper-1` defaults to `verbose_json` (duration + segments); a non-TTS/STT model id is rejected at construction.
15
+
16
+ ### Fixed
17
+
18
+ - **Non-text content parts are no longer coerced to `image_url`.** The message mapper now branches per modality (image → `image_url`, pdf → `file`, audio → `input_audio`) instead of forcing every attachment through the image path.
19
+
7
20
  ## 4.5.0 - 2026-07-01
8
21
 
9
22
  ### Fixed
package/cjs/index.cjs CHANGED
@@ -115,15 +115,62 @@ function stringifyContent(content) {
115
115
  if (typeof content === "string") return content;
116
116
  return content.filter((part) => part.type === "text").map((part) => part.text).join("");
117
117
  }
118
+ /**
119
+ * Map a resolved `ContentPart` to an OpenAI chat content part — one
120
+ * branch per modality, each to its real wire shape:
121
+ *
122
+ * - `text` → `{ type: "text" }`.
123
+ * - `image` → `{ type: "image_url" }` (remote URL, or a `data:` URL for
124
+ * inlined base64 bytes).
125
+ * - `pdf` → `{ type: "file", file: { file_data } }` (OpenAI document
126
+ * input; base64 only — there is no remote-URL file source).
127
+ * - `audio` → `{ type: "input_audio", input_audio: { data, format } }`
128
+ * (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).
129
+ *
130
+ * PDF and audio reach this point ONLY when the model declared the
131
+ * matching capability (`openai.model({ name, pdf: true })` /
132
+ * `{ audio: true }`) — the agent's modality gate throws upfront
133
+ * otherwise, so capability and behavior stay in lockstep. A remote-URL
134
+ * pdf/audio source raises a typed `InvalidRequestError` here rather
135
+ * than a downstream provider fault.
136
+ */
118
137
  function toOpenAIContentPart(part) {
119
138
  if (part.type === "text") return {
120
139
  type: "text",
121
140
  text: part.text
122
141
  };
123
- return {
142
+ if (part.type === "image") return {
124
143
  type: "image_url",
125
144
  image_url: { url: "url" in part.source ? part.source.url : `data:${part.source.mediaType};base64,${part.source.base64}` }
126
145
  };
146
+ if (part.type === "pdf") {
147
+ if ("url" in part.source) throw new _warlock_js_ai.InvalidRequestError("OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.");
148
+ return {
149
+ type: "file",
150
+ file: {
151
+ filename: "document.pdf",
152
+ file_data: `data:${part.source.mediaType};base64,${part.source.base64}`
153
+ }
154
+ };
155
+ }
156
+ if ("url" in part.source) throw new _warlock_js_ai.InvalidRequestError("OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.");
157
+ return {
158
+ type: "input_audio",
159
+ input_audio: {
160
+ data: part.source.base64,
161
+ format: toOpenAIAudioFormat(part.source.mediaType)
162
+ }
163
+ };
164
+ }
165
+ /**
166
+ * Narrow a neutral audio media type to the two formats OpenAI's
167
+ * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a
168
+ * typed `InvalidRequestError` up front rather than a provider 400.
169
+ */
170
+ function toOpenAIAudioFormat(mediaType) {
171
+ if (mediaType === "audio/wav" || mediaType === "audio/x-wav" || mediaType === "audio/wave") return "wav";
172
+ if (mediaType === "audio/mp3" || mediaType === "audio/mpeg" || mediaType === "audio/mpga") return "mp3";
173
+ throw new _warlock_js_ai.InvalidRequestError(`OpenAI input_audio supports only "wav" and "mp3"; got "${mediaType}".`);
127
174
  }
128
175
 
129
176
  //#endregion
@@ -307,7 +354,7 @@ function parseRetryAfter(headers) {
307
354
 
308
355
  //#endregion
309
356
  //#region ../@warlock.js/ai-openai/src/embedder.ts
310
- const LOG_MODULE$1 = "ai.openai";
357
+ const LOG_MODULE$4 = "ai.openai";
311
358
  /**
312
359
  * OpenAI-backed implementation of `EmbedderContract`.
313
360
  *
@@ -365,7 +412,7 @@ var OpenAIEmbedder = class {
365
412
  * plus a camelCase usage object for the caller to shape.
366
413
  */
367
414
  async request(input) {
368
- this.logger.debug(LOG_MODULE$1, "embedder.request", "embeddings.create", {
415
+ this.logger.debug(LOG_MODULE$4, "embedder.request", "embeddings.create", {
369
416
  model: this.name,
370
417
  batch: Array.isArray(input),
371
418
  count: Array.isArray(input) ? input.length : 1
@@ -379,13 +426,13 @@ var OpenAIEmbedder = class {
379
426
  });
380
427
  } catch (thrown) {
381
428
  const wrapped = wrapOpenAIError(thrown);
382
- this.logger.error(LOG_MODULE$1, "embedder.error", wrapped.message, {
429
+ this.logger.error(LOG_MODULE$4, "embedder.error", wrapped.message, {
383
430
  code: wrapped.code,
384
431
  context: wrapped.context
385
432
  });
386
433
  throw wrapped;
387
434
  }
388
- this.logger.debug(LOG_MODULE$1, "embedder.response", "embeddings.create returned", {
435
+ this.logger.debug(LOG_MODULE$4, "embedder.response", "embeddings.create returned", {
389
436
  dimensions: response.data[0]?.embedding.length,
390
437
  usage: {
391
438
  promptTokens: response.usage.prompt_tokens,
@@ -404,6 +451,154 @@ var OpenAIEmbedder = class {
404
451
  }
405
452
  };
406
453
 
454
+ //#endregion
455
+ //#region ../@warlock.js/ai-openai/src/known-image-models.ts
456
+ /**
457
+ * Model-id prefixes OpenAI exposes through the **Images** API
458
+ * (`client.images.generate`). The two live families:
459
+ *
460
+ * - `gpt-image-*` — token-metered, always returns base64 bytes (no
461
+ * `response_format` knob), supports `output_format` + `background`.
462
+ * - `dall-e-*` — per-image-metered, returns a URL or base64 via
463
+ * `response_format`.
464
+ *
465
+ * Used by {@link isOpenAIImageModel} for the construction-time guard so
466
+ * `openai.image({ name: "gpt-4o" })` fails fast with a curated error
467
+ * instead of a downstream 400 — mirroring the embedder/vision guards.
468
+ */
469
+ const OPENAI_IMAGE_MODEL_PREFIXES = ["gpt-image", "dall-e"];
470
+ /**
471
+ * True when `name` is a recognized OpenAI image-generation model. A
472
+ * prefix match (not an exact list) so dated snapshots
473
+ * (`gpt-image-1-mini`, `dall-e-3`) are covered without a maintenance
474
+ * burden every time OpenAI ships a point release.
475
+ *
476
+ * @example
477
+ * isOpenAIImageModel("gpt-image-1"); // true
478
+ * isOpenAIImageModel("dall-e-3"); // true
479
+ * isOpenAIImageModel("gpt-4o"); // false
480
+ */
481
+ function isOpenAIImageModel(name) {
482
+ return OPENAI_IMAGE_MODEL_PREFIXES.some((prefix) => name.startsWith(prefix));
483
+ }
484
+
485
+ //#endregion
486
+ //#region ../@warlock.js/ai-openai/src/image.ts
487
+ const LOG_MODULE$3 = "ai.openai";
488
+ /** Map a neutral output container to its IANA media type. */
489
+ function mediaTypeFor(format) {
490
+ switch (format) {
491
+ case "jpeg":
492
+ case "jpg": return "image/jpeg";
493
+ case "webp": return "image/webp";
494
+ default: return "image/png";
495
+ }
496
+ }
497
+ /**
498
+ * OpenAI-backed implementation of `ImageModelContract`.
499
+ *
500
+ * **Role.** Bridges the vendor-neutral `ai.image()` verb to OpenAI's
501
+ * **Images** API for the two image families OpenAI ships: the
502
+ * token-metered `gpt-image-*` models (always return base64 bytes) and
503
+ * the per-image-metered `dall-e-*` models (URL or base64). Like
504
+ * `OpenAIEmbedder`, it's a standalone primitive — no relationship to
505
+ * chat completions, tools, or the agent loop.
506
+ *
507
+ * **Capability guard.** The constructor rejects a non-image model id
508
+ * up front (`gpt-4o` → typed `InvalidRequestError`) so the mistake
509
+ * surfaces at wiring time, not as a downstream provider 400 — the
510
+ * "fail fast at construction" rule shared with the embedder/vision
511
+ * guards.
512
+ *
513
+ * **Error handling.** Raw OpenAI SDK errors are wrapped into the typed
514
+ * `@warlock.js/ai` `AIError` hierarchy via `wrapOpenAIError`, so a
515
+ * caller catches `ProviderRateLimitError` / `ContentFilterError` /
516
+ * `ProviderAuthError` rather than OpenAI's own classes. `ai.image()`
517
+ * turns those throws into `result.error`.
518
+ *
519
+ * @example
520
+ * const model = new OpenAIImageModel(client, { name: "gpt-image-1" }, "openai");
521
+ * const { images, usage } = await model.generate("a teal ceramic mug, studio light");
522
+ */
523
+ var OpenAIImageModel = class {
524
+ constructor(client, config, provider = "openai") {
525
+ this.logger = _warlock_js_logger.log;
526
+ if (!isOpenAIImageModel(config.name)) throw new _warlock_js_ai.InvalidRequestError(`"${config.name}" is not a known OpenAI image-generation model. Use a \`gpt-image-*\` or \`dall-e-*\` model with openai.image({ name }).`);
527
+ this.client = client;
528
+ this.name = config.name;
529
+ this.provider = provider;
530
+ this.pricing = config.pricing;
531
+ }
532
+ async generate(prompt, options) {
533
+ const isGptImage = this.name.startsWith("gpt-image");
534
+ const responseFormat = options?.responseFormat ?? (isGptImage ? void 0 : "b64_json");
535
+ const body = {
536
+ model: this.name,
537
+ prompt
538
+ };
539
+ if (options?.count !== void 0) body.n = options.count;
540
+ if (options?.size !== void 0) body.size = options.size;
541
+ if (options?.quality !== void 0) body.quality = options.quality;
542
+ if (!isGptImage && responseFormat) body.response_format = responseFormat;
543
+ if (isGptImage && options?.format !== void 0) body.output_format = options.format;
544
+ if (options?.background !== void 0) body.background = options.background;
545
+ this.logger.debug(LOG_MODULE$3, "image.request", "images.generate", {
546
+ model: this.name,
547
+ count: options?.count ?? 1
548
+ });
549
+ let response;
550
+ try {
551
+ response = await this.client.images.generate(body, options?.signal ? { signal: options.signal } : void 0);
552
+ } catch (thrown) {
553
+ const wrapped = wrapOpenAIError(thrown);
554
+ this.logger.error(LOG_MODULE$3, "image.error", wrapped.message, {
555
+ code: wrapped.code,
556
+ context: wrapped.context
557
+ });
558
+ throw wrapped;
559
+ }
560
+ const images = (response.data ?? []).map((image) => this.toGeneratedImage(image, options?.format));
561
+ const usage = response.usage ? {
562
+ input: response.usage.input_tokens,
563
+ output: response.usage.output_tokens,
564
+ total: response.usage.total_tokens
565
+ } : {
566
+ input: 0,
567
+ output: 0,
568
+ total: 0
569
+ };
570
+ this.logger.debug(LOG_MODULE$3, "image.response", "images.generate succeeded", {
571
+ images: images.length,
572
+ usage
573
+ });
574
+ return {
575
+ images,
576
+ usage
577
+ };
578
+ }
579
+ /**
580
+ * Normalize one OpenAI `Image` into the neutral discriminated shape.
581
+ * Base64 wins when present (gpt-image, and DALL·E in b64 mode);
582
+ * otherwise a hosted URL. A response carrying neither is a provider
583
+ * contract violation — surface it as a typed `ProviderError` rather
584
+ * than emitting a half-formed part.
585
+ */
586
+ toGeneratedImage(image, format) {
587
+ if (image.b64_json) return {
588
+ type: "base64",
589
+ base64: image.b64_json,
590
+ mediaType: mediaTypeFor(format),
591
+ ...image.revised_prompt ? { revisedPrompt: image.revised_prompt } : {}
592
+ };
593
+ if (image.url) return {
594
+ type: "url",
595
+ url: image.url,
596
+ ...image.revised_prompt ? { revisedPrompt: image.revised_prompt } : {}
597
+ };
598
+ throw new _warlock_js_ai.ProviderError("OpenAI image response contained neither base64 bytes nor a URL.");
599
+ }
600
+ };
601
+
407
602
  //#endregion
408
603
  //#region ../@warlock.js/ai-openai/src/known-reasoning-models.ts
409
604
  /**
@@ -486,7 +681,7 @@ function inferVisionCapability(modelName) {
486
681
 
487
682
  //#endregion
488
683
  //#region ../@warlock.js/ai-openai/src/model.ts
489
- const LOG_MODULE = "ai.openai";
684
+ const LOG_MODULE$2 = "ai.openai";
490
685
  /**
491
686
  * Map an explicit `responseFormat` override to the default
492
687
  * `structuredOutput` capability. Loose wire modes (`"json_object"`,
@@ -549,7 +744,9 @@ var OpenAIModel = class {
549
744
  structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
550
745
  vision: config.vision ?? inferVisionCapability(config.name),
551
746
  reasoning: config.reasoning ?? inferReasoningCapability(config.name),
552
- promptCaching: true
747
+ promptCaching: true,
748
+ pdf: config.pdf ?? false,
749
+ audio: config.audio ?? false
553
750
  };
554
751
  }
555
752
  /**
@@ -559,7 +756,7 @@ var OpenAIModel = class {
559
756
  * instance's `ModelConfig` defaults for this call only.
560
757
  */
561
758
  async complete(messages, options) {
562
- this.logger.debug(LOG_MODULE, "request", "Starting call to chat.completions", {
759
+ this.logger.debug(LOG_MODULE$2, "request", "Starting call to chat.completions", {
563
760
  model: this.name,
564
761
  messageCount: messages.length,
565
762
  streaming: false,
@@ -578,7 +775,7 @@ var OpenAIModel = class {
578
775
  }, options?.signal ? { signal: options.signal } : void 0);
579
776
  } catch (thrown) {
580
777
  const wrapped = wrapOpenAIError(thrown);
581
- this.logger.error(LOG_MODULE, "error", wrapped.message, {
778
+ this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
582
779
  code: wrapped.code,
583
780
  context: wrapped.context
584
781
  });
@@ -587,7 +784,7 @@ var OpenAIModel = class {
587
784
  const choice = response.choices[0];
588
785
  const finishReason = mapFinishReason(choice.finish_reason);
589
786
  const usage = this.extractUsage(response.usage);
590
- this.logger.debug(LOG_MODULE, "response", "call to chat.completions succeeded", {
787
+ this.logger.debug(LOG_MODULE$2, "response", "call to chat.completions succeeded", {
591
788
  finishReason,
592
789
  usage
593
790
  });
@@ -605,7 +802,7 @@ var OpenAIModel = class {
605
802
  * Callers consume it with `for await`.
606
803
  */
607
804
  async *stream(messages, options) {
608
- this.logger.debug(LOG_MODULE, "request", "Starting streaming call to chat.completions", {
805
+ this.logger.debug(LOG_MODULE$2, "request", "Starting streaming call to chat.completions", {
609
806
  model: this.name,
610
807
  messageCount: messages.length,
611
808
  streaming: true,
@@ -626,7 +823,7 @@ var OpenAIModel = class {
626
823
  }, options?.signal ? { signal: options.signal } : void 0);
627
824
  } catch (thrown) {
628
825
  const wrapped = wrapOpenAIError(thrown);
629
- this.logger.error(LOG_MODULE, "error", wrapped.message, {
826
+ this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
630
827
  code: wrapped.code,
631
828
  context: wrapped.context
632
829
  });
@@ -681,14 +878,14 @@ var OpenAIModel = class {
681
878
  }
682
879
  } catch (thrown) {
683
880
  const wrapped = wrapOpenAIError(thrown);
684
- this.logger.error(LOG_MODULE, "error", wrapped.message, {
881
+ this.logger.error(LOG_MODULE$2, "error", wrapped.message, {
685
882
  code: wrapped.code,
686
883
  context: wrapped.context
687
884
  });
688
885
  throw wrapped;
689
886
  }
690
887
  const finishReason = mapFinishReason(rawFinishReason);
691
- this.logger.debug(LOG_MODULE, "response", "Streaming call to chat.completions succeeded", {
888
+ this.logger.debug(LOG_MODULE$2, "response", "Streaming call to chat.completions succeeded", {
692
889
  finishReason,
693
890
  usage
694
891
  });
@@ -851,6 +1048,179 @@ var OpenAIModel = class {
851
1048
  }
852
1049
  };
853
1050
 
1051
+ //#endregion
1052
+ //#region ../@warlock.js/ai-openai/src/speech.ts
1053
+ const LOG_MODULE$1 = "ai.openai";
1054
+ /** Model-id prefixes OpenAI exposes through the **Speech** (TTS) API. */
1055
+ const SPEECH_MODEL_PREFIXES = [
1056
+ "tts-1",
1057
+ "gpt-4o-mini-tts",
1058
+ "gpt-audio"
1059
+ ];
1060
+ /** True when `name` is a recognized OpenAI text-to-speech model. */
1061
+ function isOpenAISpeechModel(name) {
1062
+ return SPEECH_MODEL_PREFIXES.some((prefix) => name.startsWith(prefix));
1063
+ }
1064
+ /** Map a neutral output container hint to its IANA audio media type. */
1065
+ function audioMediaType(format) {
1066
+ switch (format) {
1067
+ case "opus": return "audio/opus";
1068
+ case "aac": return "audio/aac";
1069
+ case "flac": return "audio/flac";
1070
+ case "wav": return "audio/wav";
1071
+ case "pcm": return "audio/pcm";
1072
+ default: return "audio/mpeg";
1073
+ }
1074
+ }
1075
+ /**
1076
+ * OpenAI-backed implementation of `SpeechModelContract` (text-to-speech)
1077
+ * via `audio.speech.create`. Standalone primitive — no relation to chat
1078
+ * completions or the agent loop. Consumed by the `ai.speech()` verb.
1079
+ *
1080
+ * **Capability guard.** The constructor rejects a non-TTS model id up
1081
+ * front (`tts-1` / `gpt-4o-mini-tts` only) so the mistake surfaces at
1082
+ * wiring time, mirroring the embedder / image guards.
1083
+ *
1084
+ * @example
1085
+ * const tts = new OpenAISpeechModel(client, { name: "tts-1", voice: "alloy" }, "openai");
1086
+ * const { audio } = await tts.generate("Welcome aboard.");
1087
+ */
1088
+ var OpenAISpeechModel = class {
1089
+ constructor(client, config, provider = "openai") {
1090
+ this.logger = _warlock_js_logger.log;
1091
+ if (!isOpenAISpeechModel(config.name)) throw new _warlock_js_ai.InvalidRequestError(`"${config.name}" is not a known OpenAI text-to-speech model. Use a \`tts-1\` / \`tts-1-hd\` / \`gpt-4o-mini-tts\` model with openai.speech({ name }).`);
1092
+ this.client = client;
1093
+ this.name = config.name;
1094
+ this.provider = provider;
1095
+ this.pricing = config.pricing;
1096
+ this.defaultVoice = config.voice;
1097
+ }
1098
+ async generate(text, options) {
1099
+ const format = options?.format ?? "mp3";
1100
+ this.logger.debug(LOG_MODULE$1, "speech.request", "audio.speech.create", {
1101
+ model: this.name,
1102
+ characters: text.length
1103
+ });
1104
+ let response;
1105
+ try {
1106
+ response = await this.client.audio.speech.create({
1107
+ model: this.name,
1108
+ input: text,
1109
+ voice: options?.voice ?? this.defaultVoice ?? "alloy",
1110
+ response_format: format,
1111
+ ...options?.speed !== void 0 ? { speed: options.speed } : {},
1112
+ ...options?.instructions !== void 0 ? { instructions: options.instructions } : {}
1113
+ }, options?.signal ? { signal: options.signal } : void 0);
1114
+ } catch (thrown) {
1115
+ const wrapped = wrapOpenAIError(thrown);
1116
+ this.logger.error(LOG_MODULE$1, "speech.error", wrapped.message, {
1117
+ code: wrapped.code,
1118
+ context: wrapped.context
1119
+ });
1120
+ throw wrapped;
1121
+ }
1122
+ return {
1123
+ audio: {
1124
+ type: "base64",
1125
+ base64: Buffer.from(await response.arrayBuffer()).toString("base64"),
1126
+ mediaType: audioMediaType(format)
1127
+ },
1128
+ usage: {
1129
+ input: 0,
1130
+ output: 0,
1131
+ total: 0
1132
+ },
1133
+ characters: text.length
1134
+ };
1135
+ }
1136
+ };
1137
+
1138
+ //#endregion
1139
+ //#region ../@warlock.js/ai-openai/src/transcription.ts
1140
+ const LOG_MODULE = "ai.openai";
1141
+ /** Model-id prefixes OpenAI exposes through the **Transcription** (STT) API. */
1142
+ const TRANSCRIPTION_MODEL_PREFIXES = [
1143
+ "whisper",
1144
+ "gpt-4o-transcribe",
1145
+ "gpt-4o-mini-transcribe"
1146
+ ];
1147
+ /** True when `name` is a recognized OpenAI speech-to-text model. */
1148
+ function isOpenAITranscriptionModel(name) {
1149
+ return TRANSCRIPTION_MODEL_PREFIXES.some((prefix) => name.startsWith(prefix));
1150
+ }
1151
+ /**
1152
+ * OpenAI-backed implementation of `TranscriptionModelContract`
1153
+ * (speech-to-text) via `audio.transcriptions.create`. Consumed by the
1154
+ * `ai.transcribe()` verb.
1155
+ *
1156
+ * **Response format.** Defaults to `verbose_json` for `whisper-1` (so
1157
+ * the run gets a `duration` + timestamped `segments`) and `json` for
1158
+ * the token-metered `gpt-4o-transcribe` family. Base64 audio is wrapped
1159
+ * in an uploadable via the SDK's `toFile`.
1160
+ *
1161
+ * @example
1162
+ * const stt = new OpenAITranscriptionModel(client, { name: "whisper-1" }, "openai");
1163
+ * const { text } = await stt.transcribe({ base64, mediaType: "audio/mpeg" });
1164
+ */
1165
+ var OpenAITranscriptionModel = class {
1166
+ constructor(client, config, provider = "openai") {
1167
+ this.logger = _warlock_js_logger.log;
1168
+ if (!isOpenAITranscriptionModel(config.name)) throw new _warlock_js_ai.InvalidRequestError(`"${config.name}" is not a known OpenAI transcription model. Use a \`whisper-1\` / \`gpt-4o-transcribe\` / \`gpt-4o-mini-transcribe\` model with openai.transcribe({ name }).`);
1169
+ this.client = client;
1170
+ this.name = config.name;
1171
+ this.provider = provider;
1172
+ this.pricing = config.pricing;
1173
+ }
1174
+ async transcribe(audio, options) {
1175
+ const isWhisper = this.name.startsWith("whisper");
1176
+ const format = options?.format ?? (isWhisper ? "verbose_json" : "json");
1177
+ const file = await (0, openai.toFile)(Buffer.from(audio.base64, "base64"), audio.filename ?? "audio", { type: audio.mediaType });
1178
+ this.logger.debug(LOG_MODULE, "transcription.request", "audio.transcriptions.create", {
1179
+ model: this.name,
1180
+ format
1181
+ });
1182
+ let raw;
1183
+ try {
1184
+ raw = await this.client.audio.transcriptions.create({
1185
+ model: this.name,
1186
+ file,
1187
+ response_format: format,
1188
+ ...options?.language ? { language: options.language } : {},
1189
+ ...options?.prompt ? { prompt: options.prompt } : {}
1190
+ }, options?.signal ? { signal: options.signal } : void 0);
1191
+ } catch (thrown) {
1192
+ const wrapped = wrapOpenAIError(thrown);
1193
+ this.logger.error(LOG_MODULE, "transcription.error", wrapped.message, {
1194
+ code: wrapped.code,
1195
+ context: wrapped.context
1196
+ });
1197
+ throw wrapped;
1198
+ }
1199
+ const response = raw;
1200
+ const segments = response.segments?.map((segment) => ({
1201
+ text: segment.text,
1202
+ ...segment.start !== void 0 ? { start: segment.start } : {},
1203
+ ...segment.end !== void 0 ? { end: segment.end } : {}
1204
+ }));
1205
+ const durationSeconds = response.duration ?? (response.usage?.type === "duration" ? response.usage.seconds : void 0);
1206
+ const usage = response.usage?.type === "tokens" ? {
1207
+ input: response.usage.input_tokens ?? 0,
1208
+ output: response.usage.output_tokens ?? 0,
1209
+ total: response.usage.total_tokens ?? 0
1210
+ } : {
1211
+ input: 0,
1212
+ output: 0,
1213
+ total: 0
1214
+ };
1215
+ return {
1216
+ text: response.text,
1217
+ ...segments && segments.length > 0 ? { segments } : {},
1218
+ ...durationSeconds !== void 0 ? { durationSeconds } : {},
1219
+ usage
1220
+ };
1221
+ }
1222
+ };
1223
+
854
1224
  //#endregion
855
1225
  //#region ../@warlock.js/ai-openai/src/sdk.ts
856
1226
  /**
@@ -937,9 +1307,66 @@ var OpenAISDK = class {
937
1307
  embedder(config) {
938
1308
  return new OpenAIEmbedder(this.client, config);
939
1309
  }
1310
+ /**
1311
+ * Build an `OpenAIImageModel` bound to this SDK's client for use with
1312
+ * `ai.image({ model, prompt })`. Accepts the `gpt-image-*` (token-metered)
1313
+ * and `dall-e-*` (per-image-metered) families; a non-image model id
1314
+ * is rejected at construction.
1315
+ *
1316
+ * Pricing resolution mirrors `model()`: per-model `config.pricing`
1317
+ * wins, otherwise the SDK-level registry entry keyed by `config.name`,
1318
+ * otherwise `undefined` (no cost computed). A token-priced
1319
+ * `gpt-image-1` entry can live in the same SDK registry as the chat
1320
+ * models.
1321
+ *
1322
+ * @example
1323
+ * const model = openai.image({ name: "gpt-image-1" });
1324
+ * const { data } = await ai.image({ model, prompt: "a red bicycle" });
1325
+ */
1326
+ image(config) {
1327
+ const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
1328
+ const resolvedConfig = resolvedPricing === config.pricing ? config : {
1329
+ ...config,
1330
+ pricing: resolvedPricing
1331
+ };
1332
+ return new OpenAIImageModel(this.client, resolvedConfig, this.provider);
1333
+ }
1334
+ /**
1335
+ * Build an `OpenAISpeechModel` (text-to-speech) bound to this SDK's
1336
+ * client, for use with `ai.speech({ model, text })`. Accepts the
1337
+ * `tts-1` / `gpt-4o-mini-tts` families; a non-TTS model id is rejected
1338
+ * at construction.
1339
+ *
1340
+ * @example
1341
+ * const tts = openai.speech({ name: "tts-1", voice: "alloy" });
1342
+ * const { data } = await ai.speech({ model: tts, text: "Hello" });
1343
+ */
1344
+ speech(config) {
1345
+ return new OpenAISpeechModel(this.client, config, this.provider);
1346
+ }
1347
+ /**
1348
+ * Build an `OpenAITranscriptionModel` (speech-to-text) bound to this
1349
+ * SDK's client, for use with `ai.transcribe({ model, audio })`.
1350
+ * Accepts the `whisper-1` / `gpt-4o-transcribe` families; a non-STT
1351
+ * model id is rejected at construction.
1352
+ *
1353
+ * @example
1354
+ * const stt = openai.transcribe({ name: "whisper-1" });
1355
+ * const { data } = await ai.transcribe({ model: stt, audio });
1356
+ */
1357
+ transcribe(config) {
1358
+ return new OpenAITranscriptionModel(this.client, config, this.provider);
1359
+ }
940
1360
  };
941
1361
 
942
1362
  //#endregion
1363
+ exports.OPENAI_IMAGE_MODEL_PREFIXES = OPENAI_IMAGE_MODEL_PREFIXES;
943
1364
  exports.OpenAIEmbedder = OpenAIEmbedder;
1365
+ exports.OpenAIImageModel = OpenAIImageModel;
944
1366
  exports.OpenAISDK = OpenAISDK;
1367
+ exports.OpenAISpeechModel = OpenAISpeechModel;
1368
+ exports.OpenAITranscriptionModel = OpenAITranscriptionModel;
1369
+ exports.isOpenAIImageModel = isOpenAIImageModel;
1370
+ exports.isOpenAISpeechModel = isOpenAISpeechModel;
1371
+ exports.isOpenAITranscriptionModel = isOpenAITranscriptionModel;
945
1372
  //# sourceMappingURL=index.cjs.map