@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { wrapOpenAIError } from "./utils/wrap-openai-error.mjs";
|
|
2
|
+
import "./utils/index.mjs";
|
|
3
|
+
import { toFile } from "openai";
|
|
4
|
+
import { InvalidRequestError } from "@warlock.js/ai";
|
|
5
|
+
import { log } from "@warlock.js/logger";
|
|
6
|
+
|
|
7
|
+
//#region ../@warlock.js/ai-openai/src/transcription.ts
|
|
8
|
+
const LOG_MODULE = "ai.openai";
|
|
9
|
+
/** Model-id prefixes OpenAI exposes through the **Transcription** (STT) API. */
|
|
10
|
+
const TRANSCRIPTION_MODEL_PREFIXES = [
|
|
11
|
+
"whisper",
|
|
12
|
+
"gpt-4o-transcribe",
|
|
13
|
+
"gpt-4o-mini-transcribe"
|
|
14
|
+
];
|
|
15
|
+
/** True when `name` is a recognized OpenAI speech-to-text model. */
|
|
16
|
+
function isOpenAITranscriptionModel(name) {
|
|
17
|
+
return TRANSCRIPTION_MODEL_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* OpenAI-backed implementation of `TranscriptionModelContract`
|
|
21
|
+
* (speech-to-text) via `audio.transcriptions.create`. Consumed by the
|
|
22
|
+
* `ai.transcribe()` verb.
|
|
23
|
+
*
|
|
24
|
+
* **Response format.** Defaults to `verbose_json` for `whisper-1` (so
|
|
25
|
+
* the run gets a `duration` + timestamped `segments`) and `json` for
|
|
26
|
+
* the token-metered `gpt-4o-transcribe` family. Base64 audio is wrapped
|
|
27
|
+
* in an uploadable via the SDK's `toFile`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const stt = new OpenAITranscriptionModel(client, { name: "whisper-1" }, "openai");
|
|
31
|
+
* const { text } = await stt.transcribe({ base64, mediaType: "audio/mpeg" });
|
|
32
|
+
*/
|
|
33
|
+
var OpenAITranscriptionModel = class {
|
|
34
|
+
constructor(client, config, provider = "openai") {
|
|
35
|
+
this.logger = log;
|
|
36
|
+
if (!isOpenAITranscriptionModel(config.name)) throw new 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 }).`);
|
|
37
|
+
this.client = client;
|
|
38
|
+
this.name = config.name;
|
|
39
|
+
this.provider = provider;
|
|
40
|
+
this.pricing = config.pricing;
|
|
41
|
+
}
|
|
42
|
+
async transcribe(audio, options) {
|
|
43
|
+
const isWhisper = this.name.startsWith("whisper");
|
|
44
|
+
const format = options?.format ?? (isWhisper ? "verbose_json" : "json");
|
|
45
|
+
const file = await toFile(Buffer.from(audio.base64, "base64"), audio.filename ?? "audio", { type: audio.mediaType });
|
|
46
|
+
this.logger.debug(LOG_MODULE, "transcription.request", "audio.transcriptions.create", {
|
|
47
|
+
model: this.name,
|
|
48
|
+
format
|
|
49
|
+
});
|
|
50
|
+
let raw;
|
|
51
|
+
try {
|
|
52
|
+
raw = await this.client.audio.transcriptions.create({
|
|
53
|
+
model: this.name,
|
|
54
|
+
file,
|
|
55
|
+
response_format: format,
|
|
56
|
+
...options?.language ? { language: options.language } : {},
|
|
57
|
+
...options?.prompt ? { prompt: options.prompt } : {}
|
|
58
|
+
}, options?.signal ? { signal: options.signal } : void 0);
|
|
59
|
+
} catch (thrown) {
|
|
60
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
61
|
+
this.logger.error(LOG_MODULE, "transcription.error", wrapped.message, {
|
|
62
|
+
code: wrapped.code,
|
|
63
|
+
context: wrapped.context
|
|
64
|
+
});
|
|
65
|
+
throw wrapped;
|
|
66
|
+
}
|
|
67
|
+
const response = raw;
|
|
68
|
+
const segments = response.segments?.map((segment) => ({
|
|
69
|
+
text: segment.text,
|
|
70
|
+
...segment.start !== void 0 ? { start: segment.start } : {},
|
|
71
|
+
...segment.end !== void 0 ? { end: segment.end } : {}
|
|
72
|
+
}));
|
|
73
|
+
const durationSeconds = response.duration ?? (response.usage?.type === "duration" ? response.usage.seconds : void 0);
|
|
74
|
+
const usage = response.usage?.type === "tokens" ? {
|
|
75
|
+
input: response.usage.input_tokens ?? 0,
|
|
76
|
+
output: response.usage.output_tokens ?? 0,
|
|
77
|
+
total: response.usage.total_tokens ?? 0
|
|
78
|
+
} : {
|
|
79
|
+
input: 0,
|
|
80
|
+
output: 0,
|
|
81
|
+
total: 0
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
text: response.text,
|
|
85
|
+
...segments && segments.length > 0 ? { segments } : {},
|
|
86
|
+
...durationSeconds !== void 0 ? { durationSeconds } : {},
|
|
87
|
+
usage
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { OpenAITranscriptionModel, isOpenAITranscriptionModel };
|
|
94
|
+
//# sourceMappingURL=transcription.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcription.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/transcription.ts"],"sourcesContent":["import {\n InvalidRequestError,\n type AudioInput,\n type TranscribeOptions,\n type TranscriptionModelContract,\n type TranscriptionModelPricing,\n type TranscriptionResponse,\n type TranscriptionSegment,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport OpenAI, { toFile } from \"openai\";\nimport type { OpenAITranscriptionConfig } from \"./config.type\";\nimport { wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\n/** Model-id prefixes OpenAI exposes through the **Transcription** (STT) API. */\nconst TRANSCRIPTION_MODEL_PREFIXES = [\"whisper\", \"gpt-4o-transcribe\", \"gpt-4o-mini-transcribe\"] as const;\n\n/** True when `name` is a recognized OpenAI speech-to-text model. */\nexport function isOpenAITranscriptionModel(name: string): boolean {\n return TRANSCRIPTION_MODEL_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n\n/** Defensive view over the response, whose shape varies by `response_format`. */\ntype RawTranscription = {\n text: string;\n duration?: number;\n language?: string;\n segments?: Array<{ text: string; start?: number; end?: number }>;\n usage?: {\n type?: string;\n seconds?: number;\n input_tokens?: number;\n output_tokens?: number;\n total_tokens?: number;\n };\n};\n\n/**\n * OpenAI-backed implementation of `TranscriptionModelContract`\n * (speech-to-text) via `audio.transcriptions.create`. Consumed by the\n * `ai.transcribe()` verb.\n *\n * **Response format.** Defaults to `verbose_json` for `whisper-1` (so\n * the run gets a `duration` + timestamped `segments`) and `json` for\n * the token-metered `gpt-4o-transcribe` family. Base64 audio is wrapped\n * in an uploadable via the SDK's `toFile`.\n *\n * @example\n * const stt = new OpenAITranscriptionModel(client, { name: \"whisper-1\" }, \"openai\");\n * const { text } = await stt.transcribe({ base64, mediaType: \"audio/mpeg\" });\n */\nexport class OpenAITranscriptionModel implements TranscriptionModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly pricing?: TranscriptionModelPricing;\n\n private readonly client: OpenAI;\n private readonly logger: Logger = log;\n\n public constructor(\n client: OpenAI,\n config: OpenAITranscriptionConfig,\n provider: string = \"openai\",\n ) {\n if (!isOpenAITranscriptionModel(config.name)) {\n throw new InvalidRequestError(\n `\"${config.name}\" is not a known OpenAI transcription model. ` +\n \"Use a `whisper-1` / `gpt-4o-transcribe` / `gpt-4o-mini-transcribe` model with openai.transcribe({ name }).\",\n );\n }\n\n this.client = client;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n }\n\n public async transcribe(\n audio: AudioInput,\n options?: TranscribeOptions,\n ): Promise<TranscriptionResponse> {\n const isWhisper = this.name.startsWith(\"whisper\");\n const format = options?.format ?? (isWhisper ? \"verbose_json\" : \"json\");\n\n const file = await toFile(Buffer.from(audio.base64, \"base64\"), audio.filename ?? \"audio\", {\n type: audio.mediaType,\n });\n\n this.logger.debug(LOG_MODULE, \"transcription.request\", \"audio.transcriptions.create\", {\n model: this.name,\n format,\n });\n\n let raw: unknown;\n\n try {\n raw = await this.client.audio.transcriptions.create(\n {\n model: this.name,\n file,\n response_format: format as OpenAI.Audio.TranscriptionCreateParams[\"response_format\"],\n ...(options?.language ? { language: options.language } : {}),\n ...(options?.prompt ? { prompt: options.prompt } : {}),\n } as OpenAI.Audio.TranscriptionCreateParamsNonStreaming,\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n this.logger.error(LOG_MODULE, \"transcription.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n throw wrapped;\n }\n\n const response = raw as RawTranscription;\n\n const segments: TranscriptionSegment[] | undefined = response.segments?.map((segment) => ({\n text: segment.text,\n ...(segment.start !== undefined ? { start: segment.start } : {}),\n ...(segment.end !== undefined ? { end: segment.end } : {}),\n }));\n\n const durationSeconds =\n response.duration ?? (response.usage?.type === \"duration\" ? response.usage.seconds : undefined);\n\n const usage =\n response.usage?.type === \"tokens\"\n ? {\n input: response.usage.input_tokens ?? 0,\n output: response.usage.output_tokens ?? 0,\n total: response.usage.total_tokens ?? 0,\n }\n : { input: 0, output: 0, total: 0 };\n\n return {\n text: response.text,\n ...(segments && segments.length > 0 ? { segments } : {}),\n ...(durationSeconds !== undefined ? { durationSeconds } : {}),\n usage,\n };\n }\n}\n"],"mappings":";;;;;;;AAcA,MAAM,aAAa;;AAGnB,MAAM,+BAA+B;CAAC;CAAW;CAAqB;AAAwB;;AAG9F,SAAgB,2BAA2B,MAAuB;CAChE,OAAO,6BAA6B,MAAM,WAAW,KAAK,WAAW,MAAM,CAAC;AAC9E;;;;;;;;;;;;;;;AA+BA,IAAa,2BAAb,MAA4E;CAQ1E,AAAO,YACL,QACA,QACA,WAAmB,UACnB;gBANgC;EAOhC,IAAI,CAAC,2BAA2B,OAAO,IAAI,GACzC,MAAM,IAAI,oBACR,IAAI,OAAO,KAAK,8JAElB;EAGF,KAAK,SAAS;EACd,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW;EAChB,KAAK,UAAU,OAAO;CACxB;CAEA,MAAa,WACX,OACA,SACgC;EAChC,MAAM,YAAY,KAAK,KAAK,WAAW,SAAS;EAChD,MAAM,SAAS,SAAS,WAAW,YAAY,iBAAiB;EAEhE,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK,MAAM,QAAQ,QAAQ,GAAG,MAAM,YAAY,SAAS,EACxF,MAAM,MAAM,UACd,CAAC;EAED,KAAK,OAAO,MAAM,YAAY,yBAAyB,+BAA+B;GACpF,OAAO,KAAK;GACZ;EACF,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,MAAM,MAAM,KAAK,OAAO,MAAM,eAAe,OAC3C;IACE,OAAO,KAAK;IACZ;IACA,iBAAiB;IACjB,GAAI,SAAS,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;IAC1D,GAAI,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;GACtD,GACA,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,MACjD;EACF,SAAS,QAAQ;GACf,MAAM,UAAU,gBAAgB,MAAM;GACtC,KAAK,OAAO,MAAM,YAAY,uBAAuB,QAAQ,SAAS;IACpE,MAAM,QAAQ;IACd,SAAS,QAAQ;GACnB,CAAC;GACD,MAAM;EACR;EAEA,MAAM,WAAW;EAEjB,MAAM,WAA+C,SAAS,UAAU,KAAK,aAAa;GACxF,MAAM,QAAQ;GACd,GAAI,QAAQ,UAAU,SAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;GAC9D,GAAI,QAAQ,QAAQ,SAAY,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;EAC1D,EAAE;EAEF,MAAM,kBACJ,SAAS,aAAa,SAAS,OAAO,SAAS,aAAa,SAAS,MAAM,UAAU;EAEvF,MAAM,QACJ,SAAS,OAAO,SAAS,WACrB;GACE,OAAO,SAAS,MAAM,gBAAgB;GACtC,QAAQ,SAAS,MAAM,iBAAiB;GACxC,OAAO,SAAS,MAAM,gBAAgB;EACxC,IACA;GAAE,OAAO;GAAG,QAAQ;GAAG,OAAO;EAAE;EAEtC,OAAO;GACL,MAAM,SAAS;GACf,GAAI,YAAY,SAAS,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;GACtD,GAAI,oBAAoB,SAAY,EAAE,gBAAgB,IAAI,CAAC;GAC3D;EACF;CACF;AACF"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { InvalidRequestError } from "@warlock.js/ai";
|
|
2
|
+
|
|
1
3
|
//#region ../@warlock.js/ai-openai/src/utils/to-openai-messages.ts
|
|
2
4
|
/**
|
|
3
5
|
* Convert vendor-neutral Message[] to OpenAI's chat message shape.
|
|
@@ -62,15 +64,62 @@ function stringifyContent(content) {
|
|
|
62
64
|
if (typeof content === "string") return content;
|
|
63
65
|
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Map a resolved `ContentPart` to an OpenAI chat content part — one
|
|
69
|
+
* branch per modality, each to its real wire shape:
|
|
70
|
+
*
|
|
71
|
+
* - `text` → `{ type: "text" }`.
|
|
72
|
+
* - `image` → `{ type: "image_url" }` (remote URL, or a `data:` URL for
|
|
73
|
+
* inlined base64 bytes).
|
|
74
|
+
* - `pdf` → `{ type: "file", file: { file_data } }` (OpenAI document
|
|
75
|
+
* input; base64 only — there is no remote-URL file source).
|
|
76
|
+
* - `audio` → `{ type: "input_audio", input_audio: { data, format } }`
|
|
77
|
+
* (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).
|
|
78
|
+
*
|
|
79
|
+
* PDF and audio reach this point ONLY when the model declared the
|
|
80
|
+
* matching capability (`openai.model({ name, pdf: true })` /
|
|
81
|
+
* `{ audio: true }`) — the agent's modality gate throws upfront
|
|
82
|
+
* otherwise, so capability and behavior stay in lockstep. A remote-URL
|
|
83
|
+
* pdf/audio source raises a typed `InvalidRequestError` here rather
|
|
84
|
+
* than a downstream provider fault.
|
|
85
|
+
*/
|
|
65
86
|
function toOpenAIContentPart(part) {
|
|
66
87
|
if (part.type === "text") return {
|
|
67
88
|
type: "text",
|
|
68
89
|
text: part.text
|
|
69
90
|
};
|
|
70
|
-
return {
|
|
91
|
+
if (part.type === "image") return {
|
|
71
92
|
type: "image_url",
|
|
72
93
|
image_url: { url: "url" in part.source ? part.source.url : `data:${part.source.mediaType};base64,${part.source.base64}` }
|
|
73
94
|
};
|
|
95
|
+
if (part.type === "pdf") {
|
|
96
|
+
if ("url" in part.source) throw new InvalidRequestError("OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.");
|
|
97
|
+
return {
|
|
98
|
+
type: "file",
|
|
99
|
+
file: {
|
|
100
|
+
filename: "document.pdf",
|
|
101
|
+
file_data: `data:${part.source.mediaType};base64,${part.source.base64}`
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if ("url" in part.source) throw new InvalidRequestError("OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.");
|
|
106
|
+
return {
|
|
107
|
+
type: "input_audio",
|
|
108
|
+
input_audio: {
|
|
109
|
+
data: part.source.base64,
|
|
110
|
+
format: toOpenAIAudioFormat(part.source.mediaType)
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Narrow a neutral audio media type to the two formats OpenAI's
|
|
116
|
+
* `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a
|
|
117
|
+
* typed `InvalidRequestError` up front rather than a provider 400.
|
|
118
|
+
*/
|
|
119
|
+
function toOpenAIAudioFormat(mediaType) {
|
|
120
|
+
if (mediaType === "audio/wav" || mediaType === "audio/x-wav" || mediaType === "audio/wave") return "wav";
|
|
121
|
+
if (mediaType === "audio/mp3" || mediaType === "audio/mpeg" || mediaType === "audio/mpga") return "mp3";
|
|
122
|
+
throw new InvalidRequestError(`OpenAI input_audio supports only "wav" and "mp3"; got "${mediaType}".`);
|
|
74
123
|
}
|
|
75
124
|
|
|
76
125
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["import type
|
|
1
|
+
{"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral Message[] to OpenAI's chat message shape.\n * Handles the `tool` role (requires `tool_call_id`) and assistant messages\n * that carry `toolCalls` from a prior model response.\n *\n * Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message\n * content-parts shape: text becomes `{ type: \"text\", text }`, images become\n * `{ type: \"image_url\", image_url: { url } }` — with base64 sources rendered\n * as `data:` URLs inline.\n *\n * @example\n * const openaiMessages = toOpenAIMessages([\n * { role: \"user\", content: \"Hi\" },\n * { role: \"tool\", toolCallId: \"call_1\", content: '{\"ok\":true}' },\n * ]);\n *\n * @example\n * toOpenAIMessages([\n * { role: \"user\", content: [\n * { type: \"text\", text: \"What is this?\" },\n * { type: \"image\", source: { url: \"https://example.com/cat.jpg\" } },\n * ]},\n * ]);\n */\nexport function toOpenAIMessages(\n messages: Message[],\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n return messages.map((m) => {\n if (m.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(m.content),\n tool_call_id: m.toolCallId ?? \"\",\n };\n }\n if (m.role === \"assistant\" && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(m.content),\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: { name: tc.name, arguments: JSON.stringify(tc.input ?? {}) },\n })),\n };\n }\n\n if (m.role === \"user\" && Array.isArray(m.content)) {\n return {\n role: \"user\",\n content: m.content.map(toOpenAIContentPart),\n };\n }\n\n return { role: m.role, content: stringifyContent(m.content) } as\n | OpenAI.Chat.Completions.ChatCompletionUserMessageParam\n | OpenAI.Chat.Completions.ChatCompletionSystemMessageParam\n | OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n });\n}\n\n/**\n * Multipart content is only meaningful on user messages — for any other\n * role (system / assistant text / tool), collapse a `ContentPart[]` to\n * its concatenated text so OpenAI's wire format stays valid. Plain\n * strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n\n/**\n * Map a resolved `ContentPart` to an OpenAI chat content part — one\n * branch per modality, each to its real wire shape:\n *\n * - `text` → `{ type: \"text\" }`.\n * - `image` → `{ type: \"image_url\" }` (remote URL, or a `data:` URL for\n * inlined base64 bytes).\n * - `pdf` → `{ type: \"file\", file: { file_data } }` (OpenAI document\n * input; base64 only — there is no remote-URL file source).\n * - `audio` → `{ type: \"input_audio\", input_audio: { data, format } }`\n * (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).\n *\n * PDF and audio reach this point ONLY when the model declared the\n * matching capability (`openai.model({ name, pdf: true })` /\n * `{ audio: true }`) — the agent's modality gate throws upfront\n * otherwise, so capability and behavior stay in lockstep. A remote-URL\n * pdf/audio source raises a typed `InvalidRequestError` here rather\n * than a downstream provider fault.\n */\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n if (part.type === \"image\") {\n const url =\n \"url\" in part.source\n ? part.source.url\n : `data:${part.source.mediaType};base64,${part.source.base64}`;\n\n return { type: \"image_url\", image_url: { url } };\n }\n\n if (part.type === \"pdf\") {\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.\",\n );\n }\n\n return {\n type: \"file\",\n file: {\n filename: \"document.pdf\",\n file_data: `data:${part.source.mediaType};base64,${part.source.base64}`,\n },\n };\n }\n\n // Audio — the remaining `ContentPart` variant.\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.\",\n );\n }\n\n return {\n type: \"input_audio\",\n input_audio: {\n data: part.source.base64,\n format: toOpenAIAudioFormat(part.source.mediaType),\n },\n };\n}\n\n/**\n * Narrow a neutral audio media type to the two formats OpenAI's\n * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a\n * typed `InvalidRequestError` up front rather than a provider 400.\n */\nfunction toOpenAIAudioFormat(mediaType: string): \"wav\" | \"mp3\" {\n if (mediaType === \"audio/wav\" || mediaType === \"audio/x-wav\" || mediaType === \"audio/wave\") {\n return \"wav\";\n }\n\n if (mediaType === \"audio/mp3\" || mediaType === \"audio/mpeg\" || mediaType === \"audio/mpga\") {\n return \"mp3\";\n }\n\n throw new InvalidRequestError(\n `OpenAI input_audio supports only \"wav\" and \"mp3\"; got \"${mediaType}\".`,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,iBACd,UACsD;CACtD,OAAO,SAAS,KAAK,MAAM;EACzB,IAAI,EAAE,SAAS,QACb,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,EAAE,OAAO;GACnC,cAAc,EAAE,cAAc;EAChC;EAEF,IAAI,EAAE,SAAS,eAAe,EAAE,aAAa,EAAE,UAAU,SAAS,GAChE,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,EAAE,OAAO;GACnC,YAAY,EAAE,UAAU,KAAK,QAAQ;IACnC,IAAI,GAAG;IACP,MAAM;IACN,UAAU;KAAE,MAAM,GAAG;KAAM,WAAW,KAAK,UAAU,GAAG,SAAS,CAAC,CAAC;IAAE;GACvE,EAAE;EACJ;EAGF,IAAI,EAAE,SAAS,UAAU,MAAM,QAAQ,EAAE,OAAO,GAC9C,OAAO;GACL,MAAM;GACN,SAAS,EAAE,QAAQ,IAAI,mBAAmB;EAC5C;EAGF,OAAO;GAAE,MAAM,EAAE;GAAM,SAAS,iBAAiB,EAAE,OAAO;EAAE;CAI9D,CAAC;AACH;;;;;;;AAQA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,CAAC,CAC9E,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ;;;;;;;;;;;;;;;;;;;;AAqBA,SAAS,oBAAoB,MAAsE;CACjG,IAAI,KAAK,SAAS,QAChB,OAAO;EAAE,MAAM;EAAQ,MAAM,KAAK;CAAK;CAGzC,IAAI,KAAK,SAAS,SAMhB,OAAO;EAAE,MAAM;EAAa,WAAW,EAAE,KAJvC,SAAS,KAAK,SACV,KAAK,OAAO,MACZ,QAAQ,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO,SAEb;CAAE;CAGjD,IAAI,KAAK,SAAS,OAAO;EACvB,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,8FACF;EAGF,OAAO;GACL,MAAM;GACN,MAAM;IACJ,UAAU;IACV,WAAW,QAAQ,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO;GACjE;EACF;CACF;CAGA,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,2FACF;CAGF,OAAO;EACL,MAAM;EACN,aAAa;GACX,MAAM,KAAK,OAAO;GAClB,QAAQ,oBAAoB,KAAK,OAAO,SAAS;EACnD;CACF;AACF;;;;;;AAOA,SAAS,oBAAoB,WAAkC;CAC7D,IAAI,cAAc,eAAe,cAAc,iBAAiB,cAAc,cAC5E,OAAO;CAGT,IAAI,cAAc,eAAe,cAAc,gBAAgB,cAAc,cAC3E,OAAO;CAGT,MAAM,IAAI,oBACR,0DAA0D,UAAU,GACtE;AACF"}
|
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: setup-openai
|
|
11
|
-
description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
|
|
11
|
+
description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — `@warlock.js/ai/generate-images/SKILL.md`; agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# `@warlock.js/ai-openai`
|
|
@@ -54,8 +54,10 @@ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
|
|
|
54
54
|
| `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
|
|
55
55
|
| `reasoning` | Inferred from model name. `true` for the o-series (`o1*`, `o3*`, `o4*`) and the `gpt-5*` family; `false` otherwise. Drives whether `reasoning_effort` is forwarded. |
|
|
56
56
|
| `promptCaching` | Always `true`. OpenAI caches long prompt prefixes automatically and reports hits via `usage.cachedTokens` — there are no caller-supplied write breakpoints. |
|
|
57
|
+
| `pdf` | `false` by default (opt-in `.model({ pdf: true })`). OpenAI accepts PDF `file` parts only on specific models (the `gpt-4o` family) — honest off until you set it. |
|
|
58
|
+
| `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
|
|
57
59
|
|
|
58
|
-
**Override `vision`, `structuredOutput`, or `
|
|
60
|
+
**Override `vision`, `structuredOutput`, `reasoning`, `pdf`, or `audio` explicitly** via `.model({ name, vision?, structuredOutput?, reasoning?, pdf?, audio? })` — an explicit value always wins over inference.
|
|
59
61
|
|
|
60
62
|
## Structured output
|
|
61
63
|
|
|
@@ -70,15 +72,32 @@ openai.model({ name: "some-route", responseFormat: "text" }) // no resp
|
|
|
70
72
|
|
|
71
73
|
`"json_object"` and `"text"` also flip `structuredOutput` to `false`, so the agent re-injects the schema as a soft prompt hint. Pin `structuredOutput` explicitly to override that.
|
|
72
74
|
|
|
73
|
-
## Multipart messages (
|
|
75
|
+
## Multipart messages (image / PDF / audio input)
|
|
74
76
|
|
|
75
|
-
`ContentPart[]` user content
|
|
77
|
+
`ContentPart[]` user content maps per modality to OpenAI's real wire parts:
|
|
76
78
|
|
|
77
|
-
- `{ type: "text"
|
|
79
|
+
- `{ type: "text" }` → `{ type: "text", text }`
|
|
78
80
|
- `{ type: "image", source: { url } }` → `{ type: "image_url", image_url: { url } }`
|
|
79
81
|
- `{ type: "image", source: { base64, mediaType } }` → `{ type: "image_url", image_url: { url: "data:{mediaType};base64,{base64}" } }`
|
|
82
|
+
- `{ type: "pdf", source: { base64, mediaType } }` → `{ type: "file", file: { filename, file_data: "data:{mediaType};base64,…" } }` — requires `.model({ pdf: true })`
|
|
83
|
+
- `{ type: "audio", source: { base64, mediaType } }` → `{ type: "input_audio", input_audio: { data, format: "wav" | "mp3" } }` — requires `.model({ audio: true })`
|
|
80
84
|
|
|
81
|
-
The agent prepares attachments before they reach the adapter; this package never reads files itself.
|
|
85
|
+
PDF and audio reach the wire only when the model declares the matching capability — the agent's modality gate throws otherwise, so capability ≡ behavior. A remote-URL pdf/audio source raises a typed `InvalidRequestError` (OpenAI has no remote file/audio source); an unsupported audio media type does too (only `wav` / `mp3`). The agent prepares attachments before they reach the adapter; this package never reads files itself.
|
|
86
|
+
|
|
87
|
+
## Image generation (`gpt-image` / DALL·E)
|
|
88
|
+
|
|
89
|
+
`openai.image({ name })` returns an `ImageModelContract` for the `ai.image()` verb:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const gpt = openai.image({ name: "gpt-image-1", pricing: { input: 5, output: 40 } }); // token-metered
|
|
93
|
+
const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } }); // per-image
|
|
94
|
+
|
|
95
|
+
const { data } = await ai.image({ model: gpt, prompt: "a red bicycle", size: "1024x1024" });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `gpt-image-*` is token-metered (price with `{ input, output }`) and always returns base64 bytes — the adapter never sends `response_format` (the API rejects it).
|
|
99
|
+
- `dall-e-*` is per-image (price with `{ perImage }` / `perImageBySize`); defaults to base64, opt into a hosted URL with `options: { responseFormat: "url" }`.
|
|
100
|
+
- A non-image model id (`openai.image({ name: "gpt-4o" })`) throws `InvalidRequestError` at construction. The verb surface (envelope, options, cost-truth) lives in [`@warlock.js/ai/generate-images/SKILL.md`](@warlock.js/ai/generate-images/SKILL.md).
|
|
82
101
|
|
|
83
102
|
## Streaming
|
|
84
103
|
|
package/llms.txt
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [setup-openai](@warlock.js/ai-openai/setup-openai/SKILL.md): Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.
|
|
9
|
+
- [setup-openai](@warlock.js/ai-openai/setup-openai/SKILL.md): Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — `@warlock.js/ai/generate-images/SKILL.md`; agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.
|
package/package.json
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"openai": "^6.34.0",
|
|
17
|
-
"@warlock.js/logger": "4.
|
|
17
|
+
"@warlock.js/logger": "4.6.0"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@warlock.js/ai": "4.
|
|
20
|
+
"@warlock.js/ai": "4.6.0"
|
|
21
21
|
},
|
|
22
|
-
"version": "4.
|
|
22
|
+
"version": "4.6.0",
|
|
23
23
|
"main": "./cjs/index.cjs",
|
|
24
24
|
"module": "./esm/index.mjs",
|
|
25
25
|
"types": "./esm/index.d.mts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-openai
|
|
3
|
-
description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
|
|
3
|
+
description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — `@warlock.js/ai/generate-images/SKILL.md`; agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# `@warlock.js/ai-openai`
|
|
@@ -46,8 +46,10 @@ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
|
|
|
46
46
|
| `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
|
|
47
47
|
| `reasoning` | Inferred from model name. `true` for the o-series (`o1*`, `o3*`, `o4*`) and the `gpt-5*` family; `false` otherwise. Drives whether `reasoning_effort` is forwarded. |
|
|
48
48
|
| `promptCaching` | Always `true`. OpenAI caches long prompt prefixes automatically and reports hits via `usage.cachedTokens` — there are no caller-supplied write breakpoints. |
|
|
49
|
+
| `pdf` | `false` by default (opt-in `.model({ pdf: true })`). OpenAI accepts PDF `file` parts only on specific models (the `gpt-4o` family) — honest off until you set it. |
|
|
50
|
+
| `audio` | `false` by default (opt-in `.model({ audio: true })`). Only the `gpt-4o-audio-preview` family accepts `input_audio`. |
|
|
49
51
|
|
|
50
|
-
**Override `vision`, `structuredOutput`, or `
|
|
52
|
+
**Override `vision`, `structuredOutput`, `reasoning`, `pdf`, or `audio` explicitly** via `.model({ name, vision?, structuredOutput?, reasoning?, pdf?, audio? })` — an explicit value always wins over inference.
|
|
51
53
|
|
|
52
54
|
## Structured output
|
|
53
55
|
|
|
@@ -62,15 +64,32 @@ openai.model({ name: "some-route", responseFormat: "text" }) // no resp
|
|
|
62
64
|
|
|
63
65
|
`"json_object"` and `"text"` also flip `structuredOutput` to `false`, so the agent re-injects the schema as a soft prompt hint. Pin `structuredOutput` explicitly to override that.
|
|
64
66
|
|
|
65
|
-
## Multipart messages (
|
|
67
|
+
## Multipart messages (image / PDF / audio input)
|
|
66
68
|
|
|
67
|
-
`ContentPart[]` user content
|
|
69
|
+
`ContentPart[]` user content maps per modality to OpenAI's real wire parts:
|
|
68
70
|
|
|
69
|
-
- `{ type: "text"
|
|
71
|
+
- `{ type: "text" }` → `{ type: "text", text }`
|
|
70
72
|
- `{ type: "image", source: { url } }` → `{ type: "image_url", image_url: { url } }`
|
|
71
73
|
- `{ type: "image", source: { base64, mediaType } }` → `{ type: "image_url", image_url: { url: "data:{mediaType};base64,{base64}" } }`
|
|
74
|
+
- `{ type: "pdf", source: { base64, mediaType } }` → `{ type: "file", file: { filename, file_data: "data:{mediaType};base64,…" } }` — requires `.model({ pdf: true })`
|
|
75
|
+
- `{ type: "audio", source: { base64, mediaType } }` → `{ type: "input_audio", input_audio: { data, format: "wav" | "mp3" } }` — requires `.model({ audio: true })`
|
|
72
76
|
|
|
73
|
-
The agent prepares attachments before they reach the adapter; this package never reads files itself.
|
|
77
|
+
PDF and audio reach the wire only when the model declares the matching capability — the agent's modality gate throws otherwise, so capability ≡ behavior. A remote-URL pdf/audio source raises a typed `InvalidRequestError` (OpenAI has no remote file/audio source); an unsupported audio media type does too (only `wav` / `mp3`). The agent prepares attachments before they reach the adapter; this package never reads files itself.
|
|
78
|
+
|
|
79
|
+
## Image generation (`gpt-image` / DALL·E)
|
|
80
|
+
|
|
81
|
+
`openai.image({ name })` returns an `ImageModelContract` for the `ai.image()` verb:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const gpt = openai.image({ name: "gpt-image-1", pricing: { input: 5, output: 40 } }); // token-metered
|
|
85
|
+
const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } }); // per-image
|
|
86
|
+
|
|
87
|
+
const { data } = await ai.image({ model: gpt, prompt: "a red bicycle", size: "1024x1024" });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `gpt-image-*` is token-metered (price with `{ input, output }`) and always returns base64 bytes — the adapter never sends `response_format` (the API rejects it).
|
|
91
|
+
- `dall-e-*` is per-image (price with `{ perImage }` / `perImageBySize`); defaults to base64, opt into a hosted URL with `options: { responseFormat: "url" }`.
|
|
92
|
+
- A non-image model id (`openai.image({ name: "gpt-4o" })`) throws `InvalidRequestError` at construction. The verb surface (envelope, options, cost-truth) lives in [`@warlock.js/ai/generate-images/SKILL.md`](@warlock.js/ai/generate-images/SKILL.md).
|
|
74
93
|
|
|
75
94
|
## Streaming
|
|
76
95
|
|