@warlock.js/ai-openai 4.1.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/esm/model.mjs ADDED
@@ -0,0 +1,309 @@
1
+ import { mapFinishReason } from "./utils/map-finish-reason.mjs";
2
+ import { toOpenAIMessages } from "./utils/to-openai-messages.mjs";
3
+ import { toOpenAITools } from "./utils/to-openai-tools.mjs";
4
+ import { wrapOpenAIError } from "./utils/wrap-openai-error.mjs";
5
+ import "./utils/index.mjs";
6
+ import { inferVisionCapability } from "./known-vision-models.mjs";
7
+ import { safeJsonParse } from "@warlock.js/ai";
8
+ import { log } from "@warlock.js/logger";
9
+
10
+ //#region ../../@warlock.js/ai-openai/src/model.ts
11
+ const LOG_MODULE = "ai.openai";
12
+ /**
13
+ * Map an explicit `responseFormat` override to the default
14
+ * `structuredOutput` capability. Loose wire modes (`"json_object"`,
15
+ * `"text"`) don't enforce shape, so the agent needs to see the soft
16
+ * schema hint in the system prompt — that only happens when the
17
+ * capability is `false`. Default (no override) stays `true` to
18
+ * preserve the prior assumption that OpenAI models support strict
19
+ * structured output.
20
+ */
21
+ function inferStructuredOutput(responseFormat) {
22
+ if (responseFormat === "json_object" || responseFormat === "text") return false;
23
+ return true;
24
+ }
25
+ /**
26
+ * OpenAI-backed implementation of `ModelContract`.
27
+ *
28
+ * **Role.** The provider-facing bridge between the vendor-neutral
29
+ * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,
30
+ * workflows, and supervisors never talk to OpenAI directly — they hold a
31
+ * `ModelContract`, and this class is what makes that contract concrete for
32
+ * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local
33
+ * gateways that speak the Chat Completions protocol).
34
+ *
35
+ * **Responsibility.**
36
+ * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,
37
+ * temperature, maxTokens) used as defaults for every call.
38
+ * - Owns: translating vendor-neutral `Message[]` and
39
+ * `ToolContract[]` into OpenAI wire shapes on the way out, and
40
+ * translating OpenAI's response (content, finish reason, tool calls,
41
+ * usage) back into the neutral shapes on the way in.
42
+ * - Does NOT own: dispatching tools, deciding whether to loop, tracking
43
+ * conversation history, or retrying on failure — those are agent
44
+ * concerns. The model is a stateless (per-call) protocol adapter.
45
+ *
46
+ * Because it holds a live client and shared defaults, it is modeled as a
47
+ * class (see §4.2 of code-style.md — "long-lived state across calls").
48
+ *
49
+ * @example
50
+ * import OpenAI from "openai";
51
+ * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
52
+ * const model = new OpenAIModel(client, { name: "gpt-4o", temperature: 0.3 });
53
+ *
54
+ * const myAgent = agent({
55
+ * model,
56
+ * systemPrompt: "You are a helpful assistant.",
57
+ * tools: [searchTool],
58
+ * });
59
+ *
60
+ * const result = await myAgent.execute("Summarize today's news.");
61
+ */
62
+ var OpenAIModel = class {
63
+ constructor(client, config, provider = "openai") {
64
+ this.logger = log;
65
+ this.client = client;
66
+ this.config = config;
67
+ this.name = config.name;
68
+ this.provider = provider;
69
+ this.pricing = config.pricing;
70
+ this.capabilities = {
71
+ structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
72
+ vision: config.vision ?? inferVisionCapability(config.name)
73
+ };
74
+ }
75
+ /**
76
+ * Single-shot completion. Sends the full message list to the Chat
77
+ * Completions endpoint, waits for the terminal response, and reshapes it
78
+ * into a vendor-neutral `ModelResponse`. Per-call `options` override the
79
+ * instance's `ModelConfig` defaults for this call only.
80
+ */
81
+ async complete(messages, options) {
82
+ this.logger.debug(LOG_MODULE, "request", "Starting call to chat.completions", {
83
+ model: this.name,
84
+ messageCount: messages.length,
85
+ streaming: false,
86
+ toolCount: options?.tools?.length ?? 0
87
+ });
88
+ let response;
89
+ try {
90
+ response = await this.client.chat.completions.create({
91
+ model: this.name,
92
+ messages: toOpenAIMessages(messages),
93
+ temperature: options?.temperature ?? this.config.temperature,
94
+ max_tokens: options?.maxTokens ?? this.config.maxTokens,
95
+ tools: toOpenAITools(options?.tools),
96
+ ...this.buildResponseFormat(options?.responseSchema)
97
+ }, options?.signal ? { signal: options.signal } : void 0);
98
+ } catch (thrown) {
99
+ const wrapped = wrapOpenAIError(thrown);
100
+ this.logger.error(LOG_MODULE, "error", wrapped.message, {
101
+ code: wrapped.code,
102
+ context: wrapped.context
103
+ });
104
+ throw wrapped;
105
+ }
106
+ const choice = response.choices[0];
107
+ const finishReason = mapFinishReason(choice.finish_reason);
108
+ const usage = this.extractUsage(response.usage);
109
+ this.logger.debug(LOG_MODULE, "response", "call to chat.completions succeeded", {
110
+ finishReason,
111
+ usage
112
+ });
113
+ return {
114
+ content: choice.message.content ?? "",
115
+ finishReason,
116
+ usage,
117
+ toolCalls: this.extractToolCalls(choice.message.tool_calls)
118
+ };
119
+ }
120
+ /**
121
+ * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —
122
+ * `delta` for text tokens, `tool-call` when the model requests a tool,
123
+ * and a terminal `done` carrying the final finish reason + usage totals.
124
+ * Callers consume it with `for await`.
125
+ */
126
+ async *stream(messages, options) {
127
+ this.logger.debug(LOG_MODULE, "request", "Starting streaming call to chat.completions", {
128
+ model: this.name,
129
+ messageCount: messages.length,
130
+ streaming: true,
131
+ toolCount: options?.tools?.length ?? 0
132
+ });
133
+ let stream;
134
+ try {
135
+ stream = await this.client.chat.completions.create({
136
+ model: this.name,
137
+ messages: toOpenAIMessages(messages),
138
+ temperature: options?.temperature ?? this.config.temperature,
139
+ max_tokens: options?.maxTokens ?? this.config.maxTokens,
140
+ tools: toOpenAITools(options?.tools),
141
+ stream: true,
142
+ stream_options: { include_usage: true },
143
+ ...this.buildResponseFormat(options?.responseSchema)
144
+ }, options?.signal ? { signal: options.signal } : void 0);
145
+ } catch (thrown) {
146
+ const wrapped = wrapOpenAIError(thrown);
147
+ this.logger.error(LOG_MODULE, "error", wrapped.message, {
148
+ code: wrapped.code,
149
+ context: wrapped.context
150
+ });
151
+ throw wrapped;
152
+ }
153
+ let rawFinishReason = "stop";
154
+ const usage = {
155
+ input: 0,
156
+ output: 0,
157
+ total: 0
158
+ };
159
+ const toolCallAccum = /* @__PURE__ */ new Map();
160
+ try {
161
+ for await (const chunk of stream) {
162
+ const delta = chunk.choices[0]?.delta;
163
+ const finish = chunk.choices[0]?.finish_reason;
164
+ if (delta?.content) yield {
165
+ type: "delta",
166
+ content: delta.content
167
+ };
168
+ if (delta?.tool_calls) for (const toolCall of delta.tool_calls) {
169
+ const idx = toolCall.index ?? 0;
170
+ if (!toolCallAccum.has(idx)) toolCallAccum.set(idx, {
171
+ id: "",
172
+ name: "",
173
+ arguments: ""
174
+ });
175
+ const acc = toolCallAccum.get(idx);
176
+ if (toolCall.id) acc.id = toolCall.id;
177
+ if (toolCall.function?.name) acc.name = toolCall.function.name;
178
+ if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;
179
+ }
180
+ if (finish) rawFinishReason = finish;
181
+ if (chunk.usage) {
182
+ usage.input = chunk.usage.prompt_tokens ?? 0;
183
+ usage.output = chunk.usage.completion_tokens ?? 0;
184
+ usage.total = chunk.usage.total_tokens ?? 0;
185
+ const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
186
+ if (cached !== void 0 && cached > 0) usage.cachedTokens = cached;
187
+ }
188
+ }
189
+ for (const acc of toolCallAccum.values()) {
190
+ if (!acc.name) continue;
191
+ yield {
192
+ type: "tool-call",
193
+ id: acc.id,
194
+ name: acc.name,
195
+ input: safeJsonParse(acc.arguments, {})
196
+ };
197
+ }
198
+ } catch (thrown) {
199
+ const wrapped = wrapOpenAIError(thrown);
200
+ this.logger.error(LOG_MODULE, "error", wrapped.message, {
201
+ code: wrapped.code,
202
+ context: wrapped.context
203
+ });
204
+ throw wrapped;
205
+ }
206
+ const finishReason = mapFinishReason(rawFinishReason);
207
+ this.logger.debug(LOG_MODULE, "response", "Streaming call to chat.completions succeeded", {
208
+ finishReason,
209
+ usage
210
+ });
211
+ yield {
212
+ type: "done",
213
+ finishReason,
214
+ usage
215
+ };
216
+ }
217
+ /**
218
+ * Translate the neutral `responseSchema` option into OpenAI's
219
+ * `response_format` parameter.
220
+ *
221
+ * When `config.responseFormat` is set, it wins: `"text"` emits no
222
+ * `response_format` at all, `"json_object"` always picks the loose
223
+ * mode, and `"json_schema"` picks strict mode (with the same
224
+ * `isStrictCompatible` safety check — a malformed schema still
225
+ * degrades to `json_object` rather than 400). The override exists
226
+ * because some targets (older OpenAI models, OpenRouter routes,
227
+ * Ollama OpenAI-compat) reject strict `json_schema` outright.
228
+ *
229
+ * When the override is omitted, uses strict `json_schema` mode
230
+ * (token-level enforcement) only when the schema is a proper
231
+ * root-object JSON Schema (`{ type: "object", properties: ... }`).
232
+ * For anything else — malformed extractor output, non-object
233
+ * schemas, or future shapes we haven't tested — falls back to loose
234
+ * `json_object` mode, which guarantees *some* valid JSON without
235
+ * enforcing shape. The agent's soft instruction already embeds the
236
+ * schema text in the system prompt when the model declares no
237
+ * native structured-output capability, so shape validation still
238
+ * runs client-side via the Standard Schema `validate()` call.
239
+ *
240
+ * Returns an empty spread when no schema was supplied, so the caller
241
+ * can unconditionally `...buildResponseFormat(...)` into the request.
242
+ */
243
+ buildResponseFormat(responseSchema) {
244
+ if (!responseSchema) return {};
245
+ const override = this.config.responseFormat;
246
+ if (override === "text") return {};
247
+ if (override === "json_object") return { response_format: { type: "json_object" } };
248
+ if (this.isStrictCompatible(responseSchema)) return { response_format: {
249
+ type: "json_schema",
250
+ json_schema: {
251
+ name: "response",
252
+ schema: responseSchema,
253
+ strict: true
254
+ }
255
+ } };
256
+ return { response_format: { type: "json_object" } };
257
+ }
258
+ /**
259
+ * OpenAI strict `json_schema` mode requires the root to be a JSON
260
+ * Schema object type (`{ type: "object", properties: ... }`). Anything
261
+ * else (top-level arrays, primitives, unknown shapes) is rejected with
262
+ * a 400 before a token is sampled. We check structurally here so the
263
+ * first call doesn't crash on a malformed extraction — loose
264
+ * `json_object` mode is a safe degradation.
265
+ */
266
+ isStrictCompatible(schema) {
267
+ return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null;
268
+ }
269
+ /**
270
+ * Normalize OpenAI's `usage` block (which may be absent on some responses
271
+ * or partials) into the neutral `Usage` shape. Missing usage collapses to
272
+ * zeros rather than propagating `undefined`, so downstream aggregation
273
+ * math stays safe.
274
+ */
275
+ extractUsage(raw) {
276
+ if (!raw) return {
277
+ input: 0,
278
+ output: 0,
279
+ total: 0
280
+ };
281
+ const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
282
+ return {
283
+ input: raw.prompt_tokens,
284
+ output: raw.completion_tokens,
285
+ total: raw.total_tokens,
286
+ ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {}
287
+ };
288
+ }
289
+ /**
290
+ * Reshape OpenAI's `tool_calls` array into the neutral
291
+ * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string
292
+ * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so
293
+ * malformed or empty arguments yield an empty object instead of crashing
294
+ * the trip. Returns `undefined` when no tools were requested so callers
295
+ * can branch on presence.
296
+ */
297
+ extractToolCalls(rawToolCalls) {
298
+ if (!rawToolCalls || rawToolCalls.length === 0) return;
299
+ return rawToolCalls.map((toolCall) => ({
300
+ id: toolCall.id,
301
+ name: toolCall.function.name,
302
+ input: safeJsonParse(toolCall.function.arguments, {})
303
+ }));
304
+ }
305
+ };
306
+
307
+ //#endregion
308
+ export { OpenAIModel };
309
+ //# sourceMappingURL=model.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../@warlock.js/ai-openai/src/model.ts"],"sourcesContent":["import {\n safeJsonParse,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig, OpenAIResponseFormat } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toOpenAIMessages, toOpenAITools, wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\n/**\n * Map an explicit `responseFormat` override to the default\n * `structuredOutput` capability. Loose wire modes (`\"json_object\"`,\n * `\"text\"`) don't enforce shape, so the agent needs to see the soft\n * schema hint in the system prompt — that only happens when the\n * capability is `false`. Default (no override) stays `true` to\n * preserve the prior assumption that OpenAI models support strict\n * structured output.\n */\nfunction inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: OpenAI;\n private readonly config: OpenAIModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIModelConfig, provider: string = \"openai\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),\n vision: config.vision ?? inferVisionCapability(config.name),\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const choice = response.choices[0];\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCallAccum = new Map<number, { id: string; name: string; arguments: string }>();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\" });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage.input = chunk.usage.prompt_tokens ?? 0;\n usage.output = chunk.usage.completion_tokens ?? 0;\n usage.total = chunk.usage.total_tokens ?? 0;\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n if (cached !== undefined && cached > 0) {\n usage.cachedTokens = cached;\n }\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter.\n *\n * When `config.responseFormat` is set, it wins: `\"text\"` emits no\n * `response_format` at all, `\"json_object\"` always picks the loose\n * mode, and `\"json_schema\"` picks strict mode (with the same\n * `isStrictCompatible` safety check — a malformed schema still\n * degrades to `json_object` rather than 400). The override exists\n * because some targets (older OpenAI models, OpenRouter routes,\n * Ollama OpenAI-compat) reject strict `json_schema` outright.\n *\n * When the override is omitted, uses strict `json_schema` mode\n * (token-level enforcement) only when the schema is a proper\n * root-object JSON Schema (`{ type: \"object\", properties: ... }`).\n * For anything else — malformed extractor output, non-object\n * schemas, or future shapes we haven't tested — falls back to loose\n * `json_object` mode, which guarantees *some* valid JSON without\n * enforcing shape. The agent's soft instruction already embeds the\n * schema text in the system prompt when the model declares no\n * native structured-output capability, so shape validation still\n * runs client-side via the Standard Schema `validate()` call.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n if (!responseSchema) {\n return {};\n }\n\n const override = this.config.responseFormat;\n\n if (override === \"text\") {\n return {};\n }\n\n if (override === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`.\n // The strict-compat check still applies in the explicit case —\n // a malformed / non-object schema would 400 before sampling, so\n // we degrade to `json_object` rather than crash.\n if (this.isStrictCompatible(responseSchema)) {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"response\",\n schema: responseSchema,\n strict: true,\n },\n },\n };\n }\n\n return { response_format: { type: \"json_object\" } };\n }\n\n /**\n * OpenAI strict `json_schema` mode requires the root to be a JSON\n * Schema object type (`{ type: \"object\", properties: ... }`). Anything\n * else (top-level arrays, primitives, unknown shapes) is rejected with\n * a 400 before a token is sampled. We check structurally here so the\n * first call doesn't crash on a malformed extraction — loose\n * `json_object` mode is a safe degradation.\n */\n private isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null\n );\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n const cachedTokens = raw.prompt_tokens_details?.cached_tokens;\n\n return {\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n"],"mappings":"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"}
package/esm/sdk.d.mts ADDED
@@ -0,0 +1,79 @@
1
+ import { OpenAIEmbedderConfig, OpenAIModelConfig, OpenAISDKConfig } from "./config.type.mjs";
2
+ import { EmbedderContract, ModelContract, SDKAdapterContract } from "@warlock.js/ai";
3
+
4
+ //#region ../../@warlock.js/ai-openai/src/sdk.d.ts
5
+ /**
6
+ * OpenAI-backed implementation of `SDKAdapterContract`.
7
+ *
8
+ * **Role.** The package entry point for any OpenAI-compatible provider
9
+ * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat
10
+ * Completions protocol). A single `OpenAISDK` instance holds one live
11
+ * `OpenAI` client, shared by every `ModelContract` it produces via
12
+ * `model()`. Users construct one SDK per provider/account and reuse it
13
+ * across all agents, workflows, and supervisors that target that
14
+ * provider.
15
+ *
16
+ * **Responsibility.**
17
+ * - Owns: a long-lived `OpenAI` client (authentication, base URL) and
18
+ * its lifetime scope. Factory for `OpenAIModel` instances — each
19
+ * model call gets a reference to the same client.
20
+ * - Does NOT own: anything per-call (tool execution, message history,
21
+ * streaming loop) — those live in `OpenAIModel` and the agent runtime.
22
+ *
23
+ * Modeled as a class (see §4.2 of code-style.md — "long-lived state
24
+ * across many calls"): the `OpenAI` client is heavy to construct and
25
+ * designed to be reused; keeping it on `this` makes that reuse
26
+ * explicit and aligns with the PascalCase naming convention readers
27
+ * expect from a constructor.
28
+ *
29
+ * @example
30
+ * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
31
+ * const model = openai.model({ name: "gpt-4o", temperature: 0.7 });
32
+ * const tokens = await openai.count("Hello world");
33
+ *
34
+ * @example
35
+ * // Compose into an `ai.openai` namespace for ergonomic agent wiring
36
+ * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };
37
+ * const myAgent = ai.agent({ model: ai.openai.model({ name: "gpt-4o-mini" }) });
38
+ */
39
+ declare class OpenAISDK implements SDKAdapterContract {
40
+ private readonly client;
41
+ private readonly provider;
42
+ private readonly pricing?;
43
+ constructor(config: OpenAISDKConfig);
44
+ /**
45
+ * Build an `OpenAIModel` bound to this SDK's client. Each call returns
46
+ * a fresh model instance, but all instances share the underlying
47
+ * `OpenAI` client — connection pools, rate limits, and authentication
48
+ * state stay unified across every model produced here. The SDK's
49
+ * `provider` label is forwarded so every model self-identifies as
50
+ * coming from the same upstream.
51
+ *
52
+ * Pricing resolution: per-model `config.pricing` wins; otherwise the
53
+ * SDK-level registry entry keyed by `config.name`; otherwise
54
+ * `undefined` (no cost computed).
55
+ */
56
+ model(config: OpenAIModelConfig): ModelContract;
57
+ /**
58
+ * Rough token-count estimate for a given text. Uses a
59
+ * character-heuristic (`approximateTokenCount`) from the core package
60
+ * — good enough for budgeting and quota guards, not for billing.
61
+ * Accepts an optional model id for future per-model tokenizer
62
+ * dispatch; currently ignored.
63
+ */
64
+ count(text: string, _model?: string): Promise<number>;
65
+ /**
66
+ * Build an `OpenAIEmbedder` bound to this SDK's client. Each call
67
+ * returns a fresh embedder instance sharing the same underlying
68
+ * `OpenAI` client — connection pools and authentication stay unified
69
+ * across every embedder produced here.
70
+ *
71
+ * @example
72
+ * const embedder = openai.embedder({ name: "text-embedding-3-small" });
73
+ * const { vector } = await embedder.embed("Hello world");
74
+ */
75
+ embedder(config: OpenAIEmbedderConfig): EmbedderContract;
76
+ }
77
+ //#endregion
78
+ export { OpenAISDK };
79
+ //# sourceMappingURL=sdk.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../@warlock.js/ai-openai/src/sdk.ts"],"mappings":";;;;;;AA8CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,EAAQ,eAAA;;;;;;;;;;;;;EAqBpB,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAe5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;;;;EAc5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
package/esm/sdk.mjs ADDED
@@ -0,0 +1,97 @@
1
+ import { OpenAIEmbedder } from "./embedder.mjs";
2
+ import { OpenAIModel } from "./model.mjs";
3
+ import OpenAI from "openai";
4
+ import { approximateTokenCount } from "@warlock.js/ai";
5
+
6
+ //#region ../../@warlock.js/ai-openai/src/sdk.ts
7
+ /**
8
+ * OpenAI-backed implementation of `SDKAdapterContract`.
9
+ *
10
+ * **Role.** The package entry point for any OpenAI-compatible provider
11
+ * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat
12
+ * Completions protocol). A single `OpenAISDK` instance holds one live
13
+ * `OpenAI` client, shared by every `ModelContract` it produces via
14
+ * `model()`. Users construct one SDK per provider/account and reuse it
15
+ * across all agents, workflows, and supervisors that target that
16
+ * provider.
17
+ *
18
+ * **Responsibility.**
19
+ * - Owns: a long-lived `OpenAI` client (authentication, base URL) and
20
+ * its lifetime scope. Factory for `OpenAIModel` instances — each
21
+ * model call gets a reference to the same client.
22
+ * - Does NOT own: anything per-call (tool execution, message history,
23
+ * streaming loop) — those live in `OpenAIModel` and the agent runtime.
24
+ *
25
+ * Modeled as a class (see §4.2 of code-style.md — "long-lived state
26
+ * across many calls"): the `OpenAI` client is heavy to construct and
27
+ * designed to be reused; keeping it on `this` makes that reuse
28
+ * explicit and aligns with the PascalCase naming convention readers
29
+ * expect from a constructor.
30
+ *
31
+ * @example
32
+ * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
33
+ * const model = openai.model({ name: "gpt-4o", temperature: 0.7 });
34
+ * const tokens = await openai.count("Hello world");
35
+ *
36
+ * @example
37
+ * // Compose into an `ai.openai` namespace for ergonomic agent wiring
38
+ * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };
39
+ * const myAgent = ai.agent({ model: ai.openai.model({ name: "gpt-4o-mini" }) });
40
+ */
41
+ var OpenAISDK = class {
42
+ constructor(config) {
43
+ this.client = new OpenAI({
44
+ apiKey: config.apiKey,
45
+ baseURL: config.baseURL
46
+ });
47
+ this.provider = config.provider ?? "openai";
48
+ this.pricing = config.pricing;
49
+ }
50
+ /**
51
+ * Build an `OpenAIModel` bound to this SDK's client. Each call returns
52
+ * a fresh model instance, but all instances share the underlying
53
+ * `OpenAI` client — connection pools, rate limits, and authentication
54
+ * state stay unified across every model produced here. The SDK's
55
+ * `provider` label is forwarded so every model self-identifies as
56
+ * coming from the same upstream.
57
+ *
58
+ * Pricing resolution: per-model `config.pricing` wins; otherwise the
59
+ * SDK-level registry entry keyed by `config.name`; otherwise
60
+ * `undefined` (no cost computed).
61
+ */
62
+ model(config) {
63
+ const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
64
+ const resolvedConfig = resolvedPricing === config.pricing ? config : {
65
+ ...config,
66
+ pricing: resolvedPricing
67
+ };
68
+ return new OpenAIModel(this.client, resolvedConfig, this.provider);
69
+ }
70
+ /**
71
+ * Rough token-count estimate for a given text. Uses a
72
+ * character-heuristic (`approximateTokenCount`) from the core package
73
+ * — good enough for budgeting and quota guards, not for billing.
74
+ * Accepts an optional model id for future per-model tokenizer
75
+ * dispatch; currently ignored.
76
+ */
77
+ async count(text, _model) {
78
+ return approximateTokenCount(text);
79
+ }
80
+ /**
81
+ * Build an `OpenAIEmbedder` bound to this SDK's client. Each call
82
+ * returns a fresh embedder instance sharing the same underlying
83
+ * `OpenAI` client — connection pools and authentication stay unified
84
+ * across every embedder produced here.
85
+ *
86
+ * @example
87
+ * const embedder = openai.embedder({ name: "text-embedding-3-small" });
88
+ * const { vector } = await embedder.embed("Hello world");
89
+ */
90
+ embedder(config) {
91
+ return new OpenAIEmbedder(this.client, config);
92
+ }
93
+ };
94
+
95
+ //#endregion
96
+ export { OpenAISDK };
97
+ //# sourceMappingURL=sdk.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../@warlock.js/ai-openai/src/sdk.ts"],"sourcesContent":["import OpenAI from \"openai\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type { OpenAIEmbedderConfig, OpenAIModelConfig, OpenAISDKConfig } from \"./config.type\";\nimport { OpenAIEmbedder } from \"./embedder\";\nimport { OpenAIModel } from \"./model\";\n\n/**\n * OpenAI-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for any OpenAI-compatible provider\n * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat\n * Completions protocol). A single `OpenAISDK` instance holds one live\n * `OpenAI` client, shared by every `ModelContract` it produces via\n * `model()`. Users construct one SDK per provider/account and reuse it\n * across all agents, workflows, and supervisors that target that\n * provider.\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client (authentication, base URL) and\n * its lifetime scope. Factory for `OpenAIModel` instances — each\n * model call gets a reference to the same client.\n * - Does NOT own: anything per-call (tool execution, message history,\n * streaming loop) — those live in `OpenAIModel` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"): the `OpenAI` client is heavy to construct and\n * designed to be reused; keeping it on `this` makes that reuse\n * explicit and aligns with the PascalCase naming convention readers\n * expect from a constructor.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });\n * const model = openai.model({ name: \"gpt-4o\", temperature: 0.7 });\n * const tokens = await openai.count(\"Hello world\");\n *\n * @example\n * // Compose into an `ai.openai` namespace for ergonomic agent wiring\n * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };\n * const myAgent = ai.agent({ model: ai.openai.model({ name: \"gpt-4o-mini\" }) });\n */\nexport class OpenAISDK implements SDKAdapterContract {\n private readonly client: OpenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OpenAISDKConfig) {\n this.client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n this.provider = config.provider ?? \"openai\";\n this.pricing = config.pricing;\n }\n\n /**\n * Build an `OpenAIModel` bound to this SDK's client. Each call returns\n * a fresh model instance, but all instances share the underlying\n * `OpenAI` client — connection pools, rate limits, and authentication\n * state stay unified across every model produced here. The SDK's\n * `provider` label is forwarded so every model self-identifies as\n * coming from the same upstream.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: OpenAIModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OpenAIModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate for a given text. Uses a\n * character-heuristic (`approximateTokenCount`) from the core package\n * — good enough for budgeting and quota guards, not for billing.\n * Accepts an optional model id for future per-model tokenizer\n * dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OpenAIEmbedder` bound to this SDK's client. Each call\n * returns a fresh embedder instance sharing the same underlying\n * `OpenAI` client — connection pools and authentication stay unified\n * across every embedder produced here.\n *\n * @example\n * const embedder = openai.embedder({ name: \"text-embedding-3-small\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OpenAIEmbedderConfig): EmbedderContract {\n return new OpenAIEmbedder(this.client, config);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAC1C,KAAK,SAAS,IAAI,OAAO;GACvB,QAAQ,OAAO;GACf,SAAS,OAAO;EAClB,CAAC;EACD,KAAK,WAAW,OAAO,YAAY;EACnC,KAAK,UAAU,OAAO;CACxB;;;;;;;;;;;;;CAcA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;;;;CAYA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,MAAM;CAC/C;AACF"}
@@ -0,0 +1,6 @@
1
+ import { mapFinishReason } from "./map-finish-reason.mjs";
2
+ import { toOpenAIMessages } from "./to-openai-messages.mjs";
3
+ import { toOpenAITools } from "./to-openai-tools.mjs";
4
+ import { wrapOpenAIError } from "./wrap-openai-error.mjs";
5
+
6
+ export { };
@@ -0,0 +1,22 @@
1
+ //#region ../../@warlock.js/ai-openai/src/utils/map-finish-reason.ts
2
+ const finishReasonMap = {
3
+ stop: "stop",
4
+ tool_calls: "tool_calls",
5
+ length: "length"
6
+ };
7
+ /**
8
+ * Map the raw OpenAI `finish_reason` string to the normalized FinishReason union.
9
+ * Unknown/unexpected values fall through to "error".
10
+ *
11
+ * @example
12
+ * mapFinishReason("stop"); // "stop"
13
+ * mapFinishReason("tool_calls"); // "tool_calls"
14
+ * mapFinishReason(null); // "error"
15
+ */
16
+ function mapFinishReason(raw) {
17
+ return finishReasonMap[raw ?? ""] ?? "error";
18
+ }
19
+
20
+ //#endregion
21
+ export { mapFinishReason };
22
+ //# sourceMappingURL=map-finish-reason.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-finish-reason.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/utils/map-finish-reason.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst finishReasonMap: Record<string, FinishReason> = {\n stop: \"stop\",\n tool_calls: \"tool_calls\",\n length: \"length\",\n};\n\n/**\n * Map the raw OpenAI `finish_reason` string to the normalized FinishReason union.\n * Unknown/unexpected values fall through to \"error\".\n *\n * @example\n * mapFinishReason(\"stop\"); // \"stop\"\n * mapFinishReason(\"tool_calls\"); // \"tool_calls\"\n * mapFinishReason(null); // \"error\"\n */\nexport function mapFinishReason(raw: string | null | undefined): FinishReason {\n return finishReasonMap[raw ?? \"\"] ?? \"error\";\n}\n"],"mappings":";AAEA,MAAM,kBAAgD;CACpD,MAAM;CACN,YAAY;CACZ,QAAQ;AACV;;;;;;;;;;AAWA,SAAgB,gBAAgB,KAA8C;CAC5E,OAAO,gBAAgB,OAAO,OAAO;AACvC"}
@@ -0,0 +1,78 @@
1
+ //#region ../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts
2
+ /**
3
+ * Convert vendor-neutral Message[] to OpenAI's chat message shape.
4
+ * Handles the `tool` role (requires `tool_call_id`) and assistant messages
5
+ * that carry `toolCalls` from a prior model response.
6
+ *
7
+ * Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message
8
+ * content-parts shape: text becomes `{ type: "text", text }`, images become
9
+ * `{ type: "image_url", image_url: { url } }` — with base64 sources rendered
10
+ * as `data:` URLs inline.
11
+ *
12
+ * @example
13
+ * const openaiMessages = toOpenAIMessages([
14
+ * { role: "user", content: "Hi" },
15
+ * { role: "tool", toolCallId: "call_1", content: '{"ok":true}' },
16
+ * ]);
17
+ *
18
+ * @example
19
+ * toOpenAIMessages([
20
+ * { role: "user", content: [
21
+ * { type: "text", text: "What is this?" },
22
+ * { type: "image", source: { url: "https://example.com/cat.jpg" } },
23
+ * ]},
24
+ * ]);
25
+ */
26
+ function toOpenAIMessages(messages) {
27
+ return messages.map((m) => {
28
+ if (m.role === "tool") return {
29
+ role: "tool",
30
+ content: stringifyContent(m.content),
31
+ tool_call_id: m.toolCallId ?? ""
32
+ };
33
+ if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) return {
34
+ role: "assistant",
35
+ content: stringifyContent(m.content),
36
+ tool_calls: m.toolCalls.map((tc) => ({
37
+ id: tc.id,
38
+ type: "function",
39
+ function: {
40
+ name: tc.name,
41
+ arguments: JSON.stringify(tc.input ?? {})
42
+ }
43
+ }))
44
+ };
45
+ if (m.role === "user" && Array.isArray(m.content)) return {
46
+ role: "user",
47
+ content: m.content.map(toOpenAIContentPart)
48
+ };
49
+ return {
50
+ role: m.role,
51
+ content: stringifyContent(m.content)
52
+ };
53
+ });
54
+ }
55
+ /**
56
+ * Multipart content is only meaningful on user messages — for any other
57
+ * role (system / assistant text / tool), collapse a `ContentPart[]` to
58
+ * its concatenated text so OpenAI's wire format stays valid. Plain
59
+ * strings pass through unchanged.
60
+ */
61
+ function stringifyContent(content) {
62
+ if (typeof content === "string") return content;
63
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("");
64
+ }
65
+ function toOpenAIContentPart(part) {
66
+ if (part.type === "text") return {
67
+ type: "text",
68
+ text: part.text
69
+ };
70
+ return {
71
+ type: "image_url",
72
+ image_url: { url: "url" in part.source ? part.source.url : `data:${part.source.mediaType};base64,${part.source.base64}` }
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ export { toOpenAIMessages };
78
+ //# sourceMappingURL=to-openai-messages.mjs.map