@warlock.js/ai-openai 4.4.0 → 4.6.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/CHANGELOG.md +23 -6
- package/cjs/index.cjs +445 -20
- package/cjs/index.cjs.map +1 -1
- package/esm/config.type.d.mts +50 -2
- package/esm/config.type.d.mts.map +1 -1
- package/esm/image.d.mts +51 -0
- package/esm/image.d.mts.map +1 -0
- package/esm/image.mjs +125 -0
- package/esm/image.mjs.map +1 -0
- package/esm/index.d.mts +6 -2
- package/esm/index.mjs +5 -1
- package/esm/known-image-models.d.mts +30 -0
- package/esm/known-image-models.d.mts.map +1 -0
- package/esm/known-image-models.mjs +33 -0
- package/esm/known-image-models.mjs.map +1 -0
- package/esm/model.mjs +3 -1
- package/esm/model.mjs.map +1 -1
- package/esm/sdk.d.mts +41 -2
- package/esm/sdk.d.mts.map +1 -1
- package/esm/sdk.mjs +57 -6
- package/esm/sdk.mjs.map +1 -1
- package/esm/speech.d.mts +33 -0
- package/esm/speech.d.mts.map +1 -0
- package/esm/speech.mjs +94 -0
- package/esm/speech.mjs.map +1 -0
- package/esm/transcription.d.mts +33 -0
- package/esm/transcription.d.mts.map +1 -0
- package/esm/transcription.mjs +94 -0
- package/esm/transcription.mjs.map +1 -0
- package/esm/utils/to-openai-messages.mjs +50 -1
- package/esm/utils/to-openai-messages.mjs.map +1 -1
- package/llms-full.txt +25 -6
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/setup-openai/SKILL.md +25 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,21 +4,38 @@ 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
|
+
|
|
20
|
+
## 4.5.0 - 2026-07-01
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **All upstream `ClientOptions` now reach the OpenAI client.** The SDK constructor peels off the framework-only `provider` / `pricing` keys and forwards the rest (`timeout`, `maxRetries`, `defaultHeaders`, custom `fetch`, `organization`, `project`, …) verbatim, instead of dropping everything but `apiKey` / `baseURL`.
|
|
25
|
+
|
|
7
26
|
## 4.4.0 - 2026-06-21
|
|
8
27
|
|
|
9
28
|
### Fixed
|
|
10
29
|
|
|
11
|
-
- **Strict structured-output compatibility check is now recursive.**
|
|
30
|
+
- **Strict structured-output compatibility check is now recursive.** A schema that omits a `required` property anywhere in the tree degrades to loose `json_object` instead of `400`-ing; client-side validation still enforces the full shape.
|
|
12
31
|
|
|
13
32
|
## 4.3.0 - 2026-06-21
|
|
14
33
|
|
|
15
34
|
### Added
|
|
16
35
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- `ModelCapabilities.reasoning` inferred from the model name (`o1*` / `o3*` / `o4*` / `gpt-5*`), overridable via `.model({ name, reasoning })`. New `known-reasoning-models.ts` prefix helper.
|
|
21
|
-
- `ModelCapabilities.promptCaching` advertised as always `true` (OpenAI caches automatically and reports hits via `Usage.cachedTokens`). `ModelCallOptions.cacheControl` write breakpoints are a graceful no-op.
|
|
36
|
+
- `Usage.reasoningTokens` is populated from `completion_tokens_details.reasoning_tokens` (o-series / gpt-5 hidden reasoning channel), emitted only when `> 0`.
|
|
37
|
+
- `ModelCallOptions.reasoning.effort` maps to the native `reasoning_effort` param for reasoning-capable models; `reasoning.maxTokens` has no Chat Completions equivalent.
|
|
38
|
+
- `ModelCapabilities.reasoning` is inferred from the model name (overridable via `.model(...)`); `promptCaching` is always `true` (OpenAI caches automatically), and `cacheControl` write breakpoints are a no-op.
|
|
22
39
|
|
|
23
40
|
## 4.1.15
|
|
24
41
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
/**
|
|
@@ -889,12 +1259,10 @@ var OpenAIModel = class {
|
|
|
889
1259
|
*/
|
|
890
1260
|
var OpenAISDK = class {
|
|
891
1261
|
constructor(config) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
this.provider = config.provider ?? "openai";
|
|
897
|
-
this.pricing = config.pricing;
|
|
1262
|
+
const { provider, pricing, ...clientOptions } = config;
|
|
1263
|
+
this.client = new openai.default(clientOptions);
|
|
1264
|
+
this.provider = provider ?? "openai";
|
|
1265
|
+
this.pricing = pricing;
|
|
898
1266
|
}
|
|
899
1267
|
/**
|
|
900
1268
|
* Build an `OpenAIModel` bound to this SDK's client. Each call returns
|
|
@@ -939,9 +1307,66 @@ var OpenAISDK = class {
|
|
|
939
1307
|
embedder(config) {
|
|
940
1308
|
return new OpenAIEmbedder(this.client, config);
|
|
941
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
|
+
}
|
|
942
1360
|
};
|
|
943
1361
|
|
|
944
1362
|
//#endregion
|
|
1363
|
+
exports.OPENAI_IMAGE_MODEL_PREFIXES = OPENAI_IMAGE_MODEL_PREFIXES;
|
|
945
1364
|
exports.OpenAIEmbedder = OpenAIEmbedder;
|
|
1365
|
+
exports.OpenAIImageModel = OpenAIImageModel;
|
|
946
1366
|
exports.OpenAISDK = OpenAISDK;
|
|
1367
|
+
exports.OpenAISpeechModel = OpenAISpeechModel;
|
|
1368
|
+
exports.OpenAITranscriptionModel = OpenAITranscriptionModel;
|
|
1369
|
+
exports.isOpenAIImageModel = isOpenAIImageModel;
|
|
1370
|
+
exports.isOpenAISpeechModel = isOpenAISpeechModel;
|
|
1371
|
+
exports.isOpenAITranscriptionModel = isOpenAITranscriptionModel;
|
|
947
1372
|
//# sourceMappingURL=index.cjs.map
|