@yourgpt/llm-sdk 2.6.0-beta.0 → 2.6.0-beta.2

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.
Files changed (64) hide show
  1. package/README.md +189 -0
  2. package/dist/adapters/index.d.mts +11 -13
  3. package/dist/adapters/index.d.ts +11 -13
  4. package/dist/adapters/index.js +610 -186
  5. package/dist/adapters/index.mjs +610 -186
  6. package/dist/audio-BBSwnjo8.d.mts +92 -0
  7. package/dist/audio-BBSwnjo8.d.ts +92 -0
  8. package/dist/{base-tNgbBaSo.d.mts → base-BICaQaRS.d.mts} +8 -3
  9. package/dist/{base-C58Dsr9p.d.ts → base-CpRPB8u8.d.ts} +8 -3
  10. package/dist/fallback/index.d.mts +4 -4
  11. package/dist/fallback/index.d.ts +4 -4
  12. package/dist/fallback/index.js +75 -39
  13. package/dist/fallback/index.mjs +75 -39
  14. package/dist/index.d.mts +18 -35
  15. package/dist/index.d.ts +18 -35
  16. package/dist/index.js +304 -88
  17. package/dist/index.mjs +302 -89
  18. package/dist/providers/anthropic/index.d.mts +3 -3
  19. package/dist/providers/anthropic/index.d.ts +3 -3
  20. package/dist/providers/anthropic/index.js +123 -7
  21. package/dist/providers/anthropic/index.mjs +123 -7
  22. package/dist/providers/azure/index.d.mts +3 -3
  23. package/dist/providers/azure/index.d.ts +3 -3
  24. package/dist/providers/azure/index.js +101 -4
  25. package/dist/providers/azure/index.mjs +101 -4
  26. package/dist/providers/fireworks/index.d.mts +3 -3
  27. package/dist/providers/fireworks/index.d.ts +3 -3
  28. package/dist/providers/fireworks/index.js +559 -178
  29. package/dist/providers/fireworks/index.mjs +559 -178
  30. package/dist/providers/google/index.d.mts +3 -3
  31. package/dist/providers/google/index.d.ts +3 -3
  32. package/dist/providers/google/index.js +559 -178
  33. package/dist/providers/google/index.mjs +559 -178
  34. package/dist/providers/ollama/index.d.mts +4 -4
  35. package/dist/providers/ollama/index.d.ts +4 -4
  36. package/dist/providers/ollama/index.js +87 -2
  37. package/dist/providers/ollama/index.mjs +87 -2
  38. package/dist/providers/openai/index.d.mts +25 -23
  39. package/dist/providers/openai/index.d.ts +25 -23
  40. package/dist/providers/openai/index.js +1397 -481
  41. package/dist/providers/openai/index.mjs +1396 -482
  42. package/dist/providers/openrouter/index.d.mts +3 -3
  43. package/dist/providers/openrouter/index.d.ts +3 -3
  44. package/dist/providers/openrouter/index.js +559 -178
  45. package/dist/providers/openrouter/index.mjs +559 -178
  46. package/dist/providers/togetherai/index.d.mts +3 -3
  47. package/dist/providers/togetherai/index.d.ts +3 -3
  48. package/dist/providers/togetherai/index.js +559 -178
  49. package/dist/providers/togetherai/index.mjs +559 -178
  50. package/dist/providers/xai/index.d.mts +3 -3
  51. package/dist/providers/xai/index.d.ts +3 -3
  52. package/dist/providers/xai/index.js +559 -178
  53. package/dist/providers/xai/index.mjs +559 -178
  54. package/dist/{types-B6dhnguR.d.mts → types-C3RgRbEC.d.mts} +11 -4
  55. package/dist/{types-CCxPmkmK.d.ts → types-DCFqkJAy.d.ts} +11 -4
  56. package/dist/{types-BQ31QIsA.d.ts → types-DHQEMFHA.d.mts} +3 -11
  57. package/dist/{types-BkQCSiIt.d.mts → types-Dg1Bcg1k.d.mts} +127 -3
  58. package/dist/{types-BkQCSiIt.d.ts → types-Dg1Bcg1k.d.ts} +127 -3
  59. package/dist/{types-BSSiJW2o.d.mts → types-Dikj3En4.d.ts} +3 -11
  60. package/dist/yourgpt/index.d.mts +1 -1
  61. package/dist/yourgpt/index.d.ts +1 -1
  62. package/docs/multimodal-and-audio.md +170 -0
  63. package/package.json +117 -49
  64. package/LICENSE +0 -21
@@ -1,3 +1,257 @@
1
+ // src/core/tool-output.ts
2
+ var imageTypes = /* @__PURE__ */ new Set([
3
+ "image/png",
4
+ "image/jpeg",
5
+ "image/webp",
6
+ "image/gif"
7
+ ]);
8
+ function normalizeToolOutput(value) {
9
+ const output = value;
10
+ if (output?.type !== "content" || !Array.isArray(output.content) || !output.content.length) {
11
+ throw new Error("Tool model output must contain a nonempty content array");
12
+ }
13
+ return {
14
+ type: "content",
15
+ content: output.content.map((part) => {
16
+ if (part?.type === "text" && typeof part.text === "string")
17
+ return { type: "text", text: part.text };
18
+ if (part?.type !== "image")
19
+ throw new Error("Unsupported tool output content type");
20
+ const sources = ["data", "url", "fileId"].filter((key) => key in part);
21
+ if (sources.length !== 1)
22
+ throw new Error(
23
+ "Tool image requires exactly one of data, url, or fileId"
24
+ );
25
+ if ("fileId" in part) {
26
+ if (part.provider !== "openai" || typeof part.fileId !== "string" || !part.fileId.trim())
27
+ throw new Error(
28
+ "Tool image file references must name the openai provider and fileId"
29
+ );
30
+ return { type: "image", fileId: part.fileId, provider: "openai" };
31
+ }
32
+ if ("url" in part) {
33
+ let url;
34
+ try {
35
+ url = new URL(part.url);
36
+ } catch {
37
+ throw new Error("Invalid tool image URL");
38
+ }
39
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
40
+ throw new Error(
41
+ "Tool image URL must use HTTP(S) without credentials"
42
+ );
43
+ return { type: "image", url: url.href };
44
+ }
45
+ if (!("data" in part) || !imageTypes.has(part.mediaType))
46
+ throw new Error("Unsupported tool image media type");
47
+ let data = part.data instanceof Uint8Array ? Buffer.from(part.data).toString("base64") : part.data;
48
+ if (typeof data !== "string")
49
+ throw new Error("Tool image data must be base64 or bytes");
50
+ if (data.startsWith("data:")) {
51
+ const match = /^data:([^;,]+);base64,(.+)$/.exec(data);
52
+ if (!match || match[1] !== part.mediaType)
53
+ throw new Error(
54
+ "Tool image data URL media type does not match mediaType"
55
+ );
56
+ data = match[2];
57
+ }
58
+ if (!data.length || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
59
+ throw new Error("Invalid tool image base64 data");
60
+ if (Buffer.byteLength(data, "base64") > 20 * 1024 * 1024)
61
+ throw new Error("Tool image exceeds 20 MiB");
62
+ if (Buffer.from(data, "base64").toString("base64") !== data)
63
+ throw new Error("Invalid tool image base64 data");
64
+ return { type: "image", data, mediaType: part.mediaType };
65
+ })
66
+ };
67
+ }
68
+ function toolOutputForProvider(value, provider) {
69
+ const output = normalizeToolOutput(value);
70
+ return output.content.map((part) => {
71
+ if (part.type === "text")
72
+ return {
73
+ type: provider === "responses" ? "input_text" : "text",
74
+ text: part.text
75
+ };
76
+ if (provider === "unsupported")
77
+ throw new Error(
78
+ "Image tool results require OpenAI Responses or Anthropic; this endpoint is not supported"
79
+ );
80
+ if ("fileId" in part) {
81
+ if (provider !== "responses")
82
+ throw new Error(
83
+ "OpenAI image file references cannot be sent to another provider"
84
+ );
85
+ return { type: "input_image", file_id: part.fileId, detail: "auto" };
86
+ }
87
+ if (provider === "responses")
88
+ return {
89
+ type: "input_image",
90
+ image_url: "url" in part ? part.url : `data:${part.mediaType};base64,${part.data}`,
91
+ detail: "auto"
92
+ };
93
+ return {
94
+ type: "image",
95
+ source: "url" in part ? { type: "url", url: part.url } : { type: "base64", media_type: part.mediaType, data: part.data }
96
+ };
97
+ });
98
+ }
99
+ function textToolOutput(value, fallback) {
100
+ if (value === void 0) return fallback;
101
+ return toolOutputForProvider(value, "unsupported").map((part) => part.text).join("\n");
102
+ }
103
+
104
+ // src/core/openai-parameters.ts
105
+ function capabilities(model) {
106
+ const gpt = /^gpt-(\d+)(?:\.(\d+))?(?:-(.+))?$/i.exec(model);
107
+ const major = Number(gpt?.[1] ?? 0);
108
+ const minor = Number(gpt?.[2] ?? 0);
109
+ const chat = gpt?.[3]?.startsWith("chat") ?? false;
110
+ const reasoning = /^o\d+(?:-|$)/i.test(model) || major >= 5 && !chat;
111
+ const supportsNone = major === 5 && minor >= 1 && !chat || /^gpt-6-(?:sol|luna)(?:-\d{4}-\d{2}-\d{2})?$/i.test(model);
112
+ return { major, minor, reasoning, supportsNone };
113
+ }
114
+ function requiresOpenAIResponses(model, baseUrl, options = {}) {
115
+ if (options.parameterPolicy === "passthrough") return false;
116
+ const c = capabilities(model);
117
+ if (!c.reasoning || options.reasoningEffort === "none") return false;
118
+ if (!(c.major >= 6 || c.major === 5 && c.minor >= 4)) return false;
119
+ if (!baseUrl) return true;
120
+ try {
121
+ return new URL(baseUrl).hostname === "api.openai.com";
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+ function buildOpenAIParameters(model, maxTokens, temperature, options = {}, api = "chat") {
127
+ const c = capabilities(model ?? "");
128
+ const normalize = options.parameterPolicy !== "passthrough";
129
+ const effort = options.reasoningEffort;
130
+ if (normalize && c.reasoning && effort === "none" && !c.supportsNone) {
131
+ throw new Error(`${model} does not support reasoningEffort: none`);
132
+ }
133
+ if (normalize && c.major >= 6 && effort === "minimal") {
134
+ throw new Error(`${model} does not support reasoningEffort: minimal`);
135
+ }
136
+ const result = {
137
+ [api === "responses" ? "max_output_tokens" : normalize && c.reasoning ? "max_completion_tokens" : "max_tokens"]: maxTokens
138
+ };
139
+ if (effort !== void 0) {
140
+ if (api === "responses") result.reasoning = { effort };
141
+ else result.reasoning_effort = effort;
142
+ }
143
+ if (temperature !== void 0) {
144
+ if (!normalize || !c.reasoning || effort === "none" && c.supportsNone) {
145
+ result.temperature = temperature;
146
+ } else {
147
+ (options.onWarning ?? console.warn)(
148
+ `[llm-sdk] ${model}: omitted temperature because reasoning is enabled.`
149
+ );
150
+ }
151
+ }
152
+ return result;
153
+ }
154
+
155
+ // src/core/openai-responses.ts
156
+ function resolveOpenAITransport(defaults, overrides) {
157
+ return {
158
+ api: overrides?.api ?? defaults.api ?? "auto",
159
+ responses: defaults.responses !== void 0 || overrides?.responses !== void 0 ? { ...defaults.responses, ...overrides?.responses } : void 0
160
+ };
161
+ }
162
+ function useOpenAIResponses(model, baseURL, parameters, transport2, hasTools, hasReplay, nativeToolSearch = false) {
163
+ const api = transport2.api ?? "auto";
164
+ if (!["auto", "chat", "responses"].includes(api)) {
165
+ throw new Error(`Unknown OpenAI API selection: ${api}`);
166
+ }
167
+ if (api === "chat") {
168
+ if (transport2.responses !== void 0 || nativeToolSearch) {
169
+ throw new Error(
170
+ "Responses options and native tool search require the Responses API"
171
+ );
172
+ }
173
+ return false;
174
+ }
175
+ let officialEndpoint = !baseURL;
176
+ try {
177
+ officialEndpoint = !baseURL || new URL(baseURL).hostname === "api.openai.com";
178
+ } catch {
179
+ }
180
+ return api === "responses" || transport2.responses !== void 0 || hasReplay && officialEndpoint || nativeToolSearch || hasTools && requiresOpenAIResponses(model, baseURL, parameters);
181
+ }
182
+ function responseMetadata(response) {
183
+ const output = Array.isArray(response?.output) ? response.output : [];
184
+ return {
185
+ openai: {
186
+ response: {
187
+ id: response?.id,
188
+ model: response?.model,
189
+ status: response?.status,
190
+ output,
191
+ usage: response?.usage,
192
+ store: response?.store,
193
+ refusals: output.flatMap(
194
+ (item) => item?.type === "message" && Array.isArray(item.content) ? item.content.filter(
195
+ (part) => part?.type === "refusal" && typeof part.refusal === "string"
196
+ ).map((part) => part.refusal) : []
197
+ )
198
+ }
199
+ }
200
+ };
201
+ }
202
+ function getResponseReplay(metadata) {
203
+ const response = metadata?.openai?.response;
204
+ if (response === void 0) return void 0;
205
+ if (!Array.isArray(response.output) || response.output.some((item) => !item || typeof item.type !== "string")) {
206
+ throw new Error(
207
+ "Invalid OpenAI Responses replay metadata: output must contain typed items"
208
+ );
209
+ }
210
+ return response.output;
211
+ }
212
+
213
+ // src/core/provider-error.ts
214
+ function providerErrorEvent(error, code) {
215
+ const details = error ?? {};
216
+ const event = {
217
+ type: "error",
218
+ message: error instanceof Error ? error.message : String(error),
219
+ code,
220
+ ...typeof details.status === "number" ? { status: details.status } : {},
221
+ ...typeof details.code === "string" ? { providerCode: details.code } : {},
222
+ ...typeof details.param === "string" ? { param: details.param } : {},
223
+ ...typeof details.request_id === "string" ? { requestId: details.request_id } : {}
224
+ };
225
+ Object.defineProperty(event, "cause", { value: error, enumerable: false });
226
+ return event;
227
+ }
228
+ function errorFromEvent(event) {
229
+ if (event.cause instanceof Error) return event.cause;
230
+ return Object.assign(new Error(event.message), {
231
+ status: event.status,
232
+ code: event.providerCode ?? event.code,
233
+ param: event.param,
234
+ request_id: event.requestId
235
+ });
236
+ }
237
+ function throwIfAborted(signal) {
238
+ if (!signal?.aborted) return;
239
+ const error = new Error("The operation was aborted");
240
+ error.name = "AbortError";
241
+ throw error;
242
+ }
243
+
244
+ // src/core/utils.ts
245
+ function generateId(prefix = "id") {
246
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
247
+ }
248
+ function generateMessageId() {
249
+ return generateId("msg");
250
+ }
251
+ function generateToolCallId() {
252
+ return generateId("call");
253
+ }
254
+
1
255
  // src/adapters/base.ts
2
256
  function stringifyForDebug(value) {
3
257
  return JSON.stringify(
@@ -104,16 +358,6 @@ function normalizeObjectJsonSchema(schema) {
104
358
  }
105
359
  return normalized;
106
360
  }
107
- function isOpenAIReasoningModel(modelId) {
108
- if (!modelId) return false;
109
- return /^(o1|o3|o4|gpt-5)/i.test(modelId);
110
- }
111
- function buildOpenAITokenParams(modelId, maxTokens, temperature) {
112
- if (isOpenAIReasoningModel(modelId)) {
113
- return { max_completion_tokens: maxTokens };
114
- }
115
- return { max_tokens: maxTokens, temperature };
116
- }
117
361
  function toOpenAIResponseFormat(rf) {
118
362
  if (!rf) return void 0;
119
363
  if (rf.type === "json_object") return { type: "json_object" };
@@ -198,7 +442,7 @@ function messageToOpenAIContent(message) {
198
442
  }
199
443
  return blocks;
200
444
  }
201
- function formatMessagesForOpenAI(messages, systemPrompt) {
445
+ function formatMessagesForOpenAI(messages, systemPrompt, preserveProviderMetadata = false) {
202
446
  const formatted = [];
203
447
  if (systemPrompt) {
204
448
  formatted.push({ role: "system", content: systemPrompt });
@@ -216,7 +460,8 @@ function formatMessagesForOpenAI(messages, systemPrompt) {
216
460
  const assistantMsg = {
217
461
  role: "assistant",
218
462
  // Gemini/xAI (OpenAI-compatible) reject content: "" on assistant messages with tool_calls
219
- content: hasToolCalls ? msg.content || null : msg.content
463
+ content: hasToolCalls ? msg.content || null : msg.content,
464
+ ...preserveProviderMetadata && msg.providerMetadata ? { providerMetadata: msg.providerMetadata } : {}
220
465
  };
221
466
  if (hasToolCalls) {
222
467
  assistantMsg.tool_calls = msg.tool_calls;
@@ -225,7 +470,8 @@ function formatMessagesForOpenAI(messages, systemPrompt) {
225
470
  } else if (msg.role === "tool" && msg.tool_call_id) {
226
471
  formatted.push({
227
472
  role: "tool",
228
- content: msg.content ?? "",
473
+ content: preserveProviderMetadata ? msg.content ?? "" : textToolOutput(msg.modelOutput, msg.content ?? ""),
474
+ ...preserveProviderMetadata && msg.modelOutput ? { modelOutput: msg.modelOutput } : {},
229
475
  tool_call_id: msg.tool_call_id
230
476
  });
231
477
  }
@@ -233,344 +479,61 @@ function formatMessagesForOpenAI(messages, systemPrompt) {
233
479
  return formatted;
234
480
  }
235
481
 
236
- // src/providers/openai/provider.ts
237
- var OPENAI_MODELS = {
238
- // GPT-4o series
239
- "gpt-4o": { vision: true, tools: true, jsonMode: true, maxTokens: 128e3 },
240
- "gpt-5.2": {
241
- vision: true,
242
- tools: true,
243
- jsonMode: true,
244
- maxTokens: 128e3
245
- },
246
- "gpt-4o-2024-11-20": {
247
- vision: true,
248
- tools: true,
249
- jsonMode: true,
250
- maxTokens: 128e3
251
- },
252
- "gpt-4o-2024-08-06": {
253
- vision: true,
254
- tools: true,
255
- jsonMode: true,
256
- maxTokens: 128e3
257
- },
258
- // GPT-4 Turbo
259
- "gpt-4-turbo": {
260
- vision: true,
261
- tools: true,
262
- jsonMode: true,
263
- maxTokens: 128e3
264
- },
265
- "gpt-4-turbo-preview": {
266
- vision: false,
267
- tools: true,
268
- jsonMode: true,
269
- maxTokens: 128e3
270
- },
271
- // GPT-4
272
- "gpt-4": { vision: false, tools: true, jsonMode: false, maxTokens: 8192 },
273
- "gpt-4-32k": {
274
- vision: false,
275
- tools: true,
276
- jsonMode: false,
277
- maxTokens: 32768
278
- },
279
- // GPT-3.5
280
- "gpt-3.5-turbo": {
281
- vision: false,
282
- tools: true,
283
- jsonMode: true,
284
- maxTokens: 16385
285
- },
286
- // O1 series (reasoning)
287
- o1: { vision: true, tools: false, jsonMode: false, maxTokens: 128e3 },
288
- "o1-mini": { vision: true, tools: false, jsonMode: false, maxTokens: 128e3 },
289
- "o1-preview": {
290
- vision: true,
291
- tools: false,
292
- jsonMode: false,
293
- maxTokens: 128e3
294
- },
295
- // O3 series
296
- "o3-mini": { vision: true, tools: false, jsonMode: false, maxTokens: 128e3 }
297
- };
298
- function openai(modelId, options = {}) {
299
- const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
300
- const baseURL = options.baseURL ?? "https://api.openai.com/v1";
301
- let client = null;
302
- async function getClient() {
303
- if (!client) {
482
+ // src/adapters/openai.ts
483
+ var OpenAIAdapter = class _OpenAIAdapter {
484
+ constructor(config) {
485
+ this.config = config;
486
+ this.model = config.model || "gpt-4o";
487
+ this.provider = _OpenAIAdapter.resolveProviderName(config.baseUrl);
488
+ }
489
+ static resolveProviderName(baseUrl) {
490
+ if (!baseUrl) return "openai";
491
+ if (baseUrl.includes("generativelanguage.googleapis.com")) return "google";
492
+ if (baseUrl.includes("x.ai")) return "xai";
493
+ if (baseUrl.includes("azure")) return "azure";
494
+ if (baseUrl.includes("fireworks.ai")) return "fireworks";
495
+ return "openai";
496
+ }
497
+ async getClient() {
498
+ if (!this.client) {
304
499
  const { default: OpenAI } = await import('openai');
305
- client = new OpenAI({
306
- apiKey,
307
- baseURL,
308
- organization: options.organization,
309
- defaultHeaders: options.headers
500
+ this.client = new OpenAI({
501
+ apiKey: this.config.apiKey,
502
+ baseURL: this.config.baseUrl,
503
+ organization: this.config.organization,
504
+ defaultHeaders: this.config.headers
310
505
  });
311
506
  }
312
- return client;
507
+ return this.client;
313
508
  }
314
- const modelConfig = OPENAI_MODELS[modelId] ?? OPENAI_MODELS["gpt-4o"];
315
- return {
316
- provider: "openai",
317
- modelId,
318
- capabilities: {
319
- supportsVision: modelConfig.vision,
320
- supportsTools: modelConfig.tools,
321
- supportsStreaming: true,
322
- supportsJsonMode: modelConfig.jsonMode,
323
- supportsThinking: false,
324
- supportsPDF: false,
325
- maxTokens: modelConfig.maxTokens,
326
- supportedImageTypes: modelConfig.vision ? ["image/png", "image/jpeg", "image/gif", "image/webp"] : []
327
- },
328
- async doGenerate(params) {
329
- const client2 = await getClient();
330
- const messages = formatMessagesForOpenAI2(params.messages);
331
- const response = await client2.chat.completions.create({
332
- model: modelId,
333
- messages,
334
- tools: params.tools,
335
- ...buildOpenAITokenParams(
336
- modelId,
337
- params.maxTokens,
338
- params.temperature
339
- ),
340
- response_format: toOpenAIResponseFormat(params.responseFormat)
341
- });
342
- const choice = response.choices[0];
343
- const message = choice.message;
344
- const toolCalls = (message.tool_calls ?? []).map(
345
- (tc) => ({
346
- id: tc.id,
347
- name: tc.function.name,
348
- args: JSON.parse(tc.function.arguments || "{}")
349
- })
350
- );
351
- return {
352
- text: message.content ?? "",
353
- toolCalls,
354
- finishReason: mapFinishReason(choice.finish_reason),
355
- usage: {
356
- promptTokens: response.usage?.prompt_tokens ?? 0,
357
- completionTokens: response.usage?.completion_tokens ?? 0,
358
- totalTokens: response.usage?.total_tokens ?? 0
359
- },
360
- rawResponse: response
361
- };
362
- },
363
- async *doStream(params) {
364
- const client2 = await getClient();
365
- const messages = formatMessagesForOpenAI2(params.messages);
366
- const stream = await client2.chat.completions.create({
367
- model: modelId,
368
- messages,
369
- tools: params.tools,
370
- ...buildOpenAITokenParams(
371
- modelId,
372
- params.maxTokens,
373
- params.temperature
374
- ),
375
- response_format: toOpenAIResponseFormat(params.responseFormat),
376
- stream: true
377
- });
378
- let currentToolCall = null;
379
- let totalPromptTokens = 0;
380
- let totalCompletionTokens = 0;
381
- for await (const chunk of stream) {
382
- if (params.signal?.aborted) {
383
- yield { type: "error", error: new Error("Aborted") };
384
- return;
385
- }
386
- const choice = chunk.choices[0];
387
- const delta = choice?.delta;
388
- if (delta?.content) {
389
- yield { type: "text-delta", text: delta.content };
390
- }
391
- if (delta?.tool_calls) {
392
- for (const tc of delta.tool_calls) {
393
- if (tc.id) {
394
- if (currentToolCall) {
395
- yield {
396
- type: "tool-call",
397
- toolCall: {
398
- id: currentToolCall.id,
399
- name: currentToolCall.name,
400
- args: JSON.parse(currentToolCall.arguments || "{}")
401
- }
402
- };
403
- }
404
- currentToolCall = {
405
- id: tc.id,
406
- name: tc.function?.name ?? "",
407
- arguments: tc.function?.arguments ?? ""
408
- };
409
- } else if (currentToolCall && tc.function?.arguments) {
410
- currentToolCall.arguments += tc.function.arguments;
411
- }
412
- }
413
- }
414
- if (choice?.finish_reason) {
415
- if (currentToolCall) {
416
- yield {
417
- type: "tool-call",
418
- toolCall: {
419
- id: currentToolCall.id,
420
- name: currentToolCall.name,
421
- args: JSON.parse(currentToolCall.arguments || "{}")
422
- }
423
- };
424
- currentToolCall = null;
425
- }
426
- if (chunk.usage) {
427
- totalPromptTokens = chunk.usage.prompt_tokens;
428
- totalCompletionTokens = chunk.usage.completion_tokens;
429
- }
430
- yield {
431
- type: "finish",
432
- finishReason: mapFinishReason(choice.finish_reason),
433
- usage: {
434
- promptTokens: totalPromptTokens,
435
- completionTokens: totalCompletionTokens,
436
- totalTokens: totalPromptTokens + totalCompletionTokens
437
- }
438
- };
439
- }
440
- }
441
- }
442
- };
443
- }
444
- function mapFinishReason(reason) {
445
- switch (reason) {
446
- case "stop":
447
- return "stop";
448
- case "length":
449
- return "length";
450
- case "tool_calls":
451
- case "function_call":
452
- return "tool-calls";
453
- case "content_filter":
454
- return "content-filter";
455
- default:
456
- return "unknown";
457
- }
458
- }
459
- function formatMessagesForOpenAI2(messages) {
460
- return messages.map((msg) => {
461
- switch (msg.role) {
462
- case "system":
463
- return { role: "system", content: msg.content };
464
- case "user":
465
- if (typeof msg.content === "string") {
466
- return { role: "user", content: msg.content };
467
- }
468
- return {
469
- role: "user",
470
- content: msg.content.map((part) => {
471
- if (part.type === "text") {
472
- return { type: "text", text: part.text };
473
- }
474
- if (part.type === "image") {
475
- const imageData = typeof part.image === "string" ? part.image : Buffer.from(part.image).toString("base64");
476
- const url = imageData.startsWith("data:") ? imageData : `data:${part.mimeType ?? "image/png"};base64,${imageData}`;
477
- return { type: "image_url", image_url: { url, detail: "auto" } };
478
- }
479
- return { type: "text", text: "" };
480
- })
481
- };
482
- case "assistant":
483
- const assistantMsg = {
484
- role: "assistant",
485
- content: msg.content
486
- };
487
- if (msg.toolCalls && msg.toolCalls.length > 0) {
488
- assistantMsg.tool_calls = msg.toolCalls.map((tc) => ({
489
- id: tc.id,
490
- type: "function",
491
- function: {
492
- name: tc.name,
493
- arguments: JSON.stringify(tc.args)
494
- }
495
- }));
496
- }
497
- return assistantMsg;
498
- case "tool":
499
- return {
500
- role: "tool",
501
- tool_call_id: msg.toolCallId,
502
- content: msg.content
503
- };
504
- default:
505
- return msg;
506
- }
507
- });
508
- }
509
-
510
- // src/core/utils.ts
511
- function generateId(prefix = "id") {
512
- return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
513
- }
514
- function generateMessageId() {
515
- return generateId("msg");
516
- }
517
- function generateToolCallId() {
518
- return generateId("call");
519
- }
520
-
521
- // src/adapters/openai.ts
522
- var OpenAIAdapter = class _OpenAIAdapter {
523
- constructor(config) {
524
- this.config = config;
525
- this.model = config.model || "gpt-4o";
526
- this.provider = _OpenAIAdapter.resolveProviderName(config.baseUrl);
527
- }
528
- static resolveProviderName(baseUrl) {
529
- if (!baseUrl) return "openai";
530
- if (baseUrl.includes("generativelanguage.googleapis.com")) return "google";
531
- if (baseUrl.includes("x.ai")) return "xai";
532
- if (baseUrl.includes("azure")) return "azure";
533
- if (baseUrl.includes("fireworks.ai")) return "fireworks";
534
- return "openai";
509
+ parameterOptions(request) {
510
+ return {
511
+ ...this.config,
512
+ reasoningEffort: request.config?.reasoningEffort ?? this.config.reasoningEffort
513
+ };
535
514
  }
536
- async getClient() {
537
- if (!this.client) {
538
- const { default: OpenAI } = await import('openai');
539
- this.client = new OpenAI({
540
- apiKey: this.config.apiKey,
541
- baseURL: this.config.baseUrl
542
- });
543
- }
544
- return this.client;
515
+ toolDefinitions(request) {
516
+ if (request.toolDefinitions?.length) return request.toolDefinitions;
517
+ return formatTools(request.actions ?? []).map((tool) => ({
518
+ name: tool.function.name,
519
+ description: tool.function.description ?? "",
520
+ location: "server",
521
+ inputSchema: tool.function.parameters
522
+ }));
545
523
  }
546
524
  shouldUseResponsesApi(request) {
547
- const hasTools = Array.isArray(request.toolDefinitions) && request.toolDefinitions.length > 0;
548
- if (!hasTools) return false;
549
- if (request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true && request.providerToolOptions.openai.nativeToolSearch.useResponsesApi !== false) {
550
- return true;
551
- }
552
- return this.requiresResponsesApiForTools(
553
- request.config?.model || this.model
525
+ return useOpenAIResponses(
526
+ request.config?.model || this.model,
527
+ this.config.baseUrl,
528
+ this.parameterOptions(request),
529
+ resolveOpenAITransport(this.config, request.config?.openai),
530
+ this.toolDefinitions(request).length > 0,
531
+ !!(request.rawMessages ?? request.messages)?.some(
532
+ (message) => message.providerMetadata?.openai?.response
533
+ ),
534
+ request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true && request.providerToolOptions.openai.nativeToolSearch.useResponsesApi !== false
554
535
  );
555
536
  }
556
- /**
557
- * Whether this model rejects function tools on /v1/chat/completions.
558
- *
559
- * Applies to OpenAI proper only — the adapter is reused for OpenAI-compatible
560
- * providers (Azure, xAI, Google, Fireworks) whose endpoints have no such
561
- * restriction and may not implement /v1/responses at all.
562
- */
563
- requiresResponsesApiForTools(modelId) {
564
- if (!modelId) return false;
565
- if (!/^gpt-5\.(?:[4-9]|\d{2,})/i.test(modelId)) return false;
566
- const baseUrl = this.config.baseUrl;
567
- if (!baseUrl) return true;
568
- try {
569
- return new URL(baseUrl).hostname === "api.openai.com";
570
- } catch {
571
- return false;
572
- }
573
- }
574
537
  /**
575
538
  * Convert one message's content to Responses content parts.
576
539
  *
@@ -601,11 +564,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
601
564
  });
602
565
  }
603
566
  buildResponsesInput(request) {
604
- const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? request.rawMessages : formatMessagesForOpenAI(request.messages, void 0);
567
+ const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? this.formatRawMessages(request.rawMessages, true) : formatMessagesForOpenAI(request.messages, void 0, true);
605
568
  const input = [];
606
569
  for (const message of sourceMessages) {
607
570
  if (message.role === "system") {
608
- if (!request.systemPrompt && message.content) {
571
+ if (message.content && message.content !== request.systemPrompt) {
609
572
  input.push({
610
573
  type: "message",
611
574
  role: "developer",
@@ -615,6 +578,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
615
578
  continue;
616
579
  }
617
580
  if (message.role === "assistant") {
581
+ const replay = getResponseReplay(message.providerMetadata);
582
+ if (replay) {
583
+ input.push(...replay);
584
+ continue;
585
+ }
618
586
  const content = this.toResponsesContent(message.content, "assistant");
619
587
  if (typeof content === "string" ? content : Array.isArray(content) && content.length > 0) {
620
588
  input.push({
@@ -638,7 +606,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
638
606
  input.push({
639
607
  type: "function_call_output",
640
608
  call_id: message.tool_call_id,
641
- output: typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null)
609
+ output: message.modelOutput ? toolOutputForProvider(message.modelOutput, "responses") : typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null)
642
610
  });
643
611
  continue;
644
612
  }
@@ -651,6 +619,32 @@ var OpenAIAdapter = class _OpenAIAdapter {
651
619
  }
652
620
  return input;
653
621
  }
622
+ /** Preserve Copilot screenshot attachments on every OpenAI transport. */
623
+ formatRawMessages(messages, preserveProviderMetadata = false) {
624
+ return messages.map((message) => {
625
+ const { attachments, providerMetadata, modelOutput, ...rest } = message;
626
+ let content = message.role === "tool" && !preserveProviderMetadata ? textToolOutput(modelOutput, String(message.content ?? "")) : message.content;
627
+ if (preserveProviderMetadata && modelOutput)
628
+ rest.modelOutput = modelOutput;
629
+ if (message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0 && content === "") {
630
+ content = null;
631
+ }
632
+ if (Array.isArray(attachments) && attachments.length > 0) {
633
+ const images = attachments.filter((attachment) => attachment && typeof attachment === "object").map(
634
+ (attachment) => attachmentToOpenAIImage(attachment)
635
+ ).filter((image) => image !== null);
636
+ if (images.length > 0) {
637
+ const parts = Array.isArray(content) ? content : content ? [{ type: "text", text: content }] : [];
638
+ content = [...parts, ...images];
639
+ }
640
+ }
641
+ return {
642
+ ...rest,
643
+ content,
644
+ ...preserveProviderMetadata && providerMetadata ? { providerMetadata } : {}
645
+ };
646
+ });
647
+ }
654
648
  /**
655
649
  * Build the web_search tool for the Responses API, or undefined when web
656
650
  * search is off.
@@ -729,20 +723,25 @@ var OpenAIAdapter = class _OpenAIAdapter {
729
723
  ).map((part) => part.text) : []
730
724
  ).join("") : "";
731
725
  const content = textFromOutput || (typeof response?.output_text === "string" ? response.output_text : "");
732
- const toolCalls = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "function_call").map((item) => ({
733
- id: item.call_id ?? item.id ?? generateToolCallId(),
734
- name: item.name,
735
- args: (() => {
736
- try {
737
- return JSON.parse(item.arguments ?? "{}");
738
- } catch {
739
- return {};
740
- }
741
- })()
742
- })) : [];
726
+ const seenCallIds = /* @__PURE__ */ new Set();
727
+ const toolCalls = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "function_call").map((item) => {
728
+ if (typeof item.call_id !== "string" || !item.call_id || typeof item.name !== "string" || !item.name || typeof item.arguments !== "string" || item.status && item.status !== "completed") {
729
+ throw new Error("Responses contains an incomplete function call");
730
+ }
731
+ if (seenCallIds.has(item.call_id))
732
+ throw new Error("Responses contains duplicate function call IDs");
733
+ seenCallIds.add(item.call_id);
734
+ const args = JSON.parse(item.arguments);
735
+ if (!args || typeof args !== "object" || Array.isArray(args))
736
+ throw new Error(
737
+ "Responses function arguments must be a JSON object"
738
+ );
739
+ return { id: item.call_id, name: item.name, args };
740
+ }) : [];
743
741
  return {
744
742
  content,
745
743
  toolCalls,
744
+ providerMetadata: responseMetadata(response),
746
745
  usage: response?.usage ? {
747
746
  promptTokens: response.usage.input_tokens ?? 0,
748
747
  completionTokens: response.usage.output_tokens ?? 0,
@@ -753,6 +752,17 @@ var OpenAIAdapter = class _OpenAIAdapter {
753
752
  }
754
753
  buildResponsesPayload(request, stream) {
755
754
  const openaiToolOptions = request.providerToolOptions?.openai;
755
+ const options = resolveOpenAITransport(
756
+ this.config,
757
+ request.config?.openai
758
+ ).responses;
759
+ const parameters = buildOpenAIParameters(
760
+ request.config?.model || this.model,
761
+ request.config?.maxTokens ?? this.config.maxTokens,
762
+ request.config?.temperature ?? this.config.temperature,
763
+ this.parameterOptions(request),
764
+ "responses"
765
+ );
756
766
  const responsesTextFormat = toOpenAIResponsesTextFormat(
757
767
  request.config?.responseFormat
758
768
  );
@@ -762,18 +772,32 @@ var OpenAIAdapter = class _OpenAIAdapter {
762
772
  instructions: request.systemPrompt,
763
773
  input: this.buildResponsesInput(request),
764
774
  tools: this.buildResponsesTools(
765
- request.toolDefinitions ?? [],
775
+ this.toolDefinitions(request),
766
776
  request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true,
767
777
  this.buildResponsesWebSearchTool(request)
768
778
  ),
769
- tool_choice: openaiToolOptions?.toolChoice === "required" ? "required" : openaiToolOptions?.toolChoice === "auto" ? "auto" : void 0,
779
+ tool_choice: openaiToolOptions?.toolChoice,
770
780
  parallel_tool_calls: openaiToolOptions?.parallelToolCalls,
771
- // Reasoning models reject `temperature` outright — sending it would swap one
772
- // 400 for another. isOpenAIReasoningModel already encodes that family test.
773
- ...isOpenAIReasoningModel(modelIdForPayload) ? {} : {
774
- temperature: request.config?.temperature ?? this.config.temperature
775
- },
776
- max_output_tokens: request.config?.maxTokens ?? this.config.maxTokens,
781
+ ...parameters,
782
+ ...options?.reasoningSummary ? {
783
+ reasoning: {
784
+ ...parameters.reasoning,
785
+ summary: options.reasoningSummary
786
+ }
787
+ } : {},
788
+ ...options?.store !== void 0 ? { store: options.store } : {},
789
+ ...options?.previousResponseId !== void 0 ? { previous_response_id: options.previousResponseId } : {},
790
+ ...options?.promptCacheKey !== void 0 ? { prompt_cache_key: options.promptCacheKey } : {},
791
+ ...options?.promptCacheOptions !== void 0 ? { prompt_cache_options: options.promptCacheOptions } : {},
792
+ ...options?.promptCacheRetention !== void 0 ? { prompt_cache_retention: options.promptCacheRetention } : {},
793
+ ...options?.store === false || options?.include ? {
794
+ include: [
795
+ .../* @__PURE__ */ new Set([
796
+ ...options?.include ?? [],
797
+ ...options?.store === false ? ["reasoning.encrypted_content"] : []
798
+ ])
799
+ ]
800
+ } : {},
777
801
  ...responsesTextFormat ? { text: { format: responsesTextFormat } } : {},
778
802
  stream
779
803
  };
@@ -781,10 +805,26 @@ var OpenAIAdapter = class _OpenAIAdapter {
781
805
  }
782
806
  async completeWithResponses(request) {
783
807
  const client = await this.getClient();
808
+ throwIfAborted(request.signal);
784
809
  const payload = this.buildResponsesPayload(request, false);
785
810
  logProviderPayload("openai", "request payload", payload, request.debug);
786
- const response = await client.responses.create(payload);
811
+ if (!client.responses?.create)
812
+ throw new Error(
813
+ "This OpenAI client does not support Responses; install an openai version with Responses support."
814
+ );
815
+ const response = await client.responses.create(payload, {
816
+ signal: request.signal
817
+ });
818
+ throwIfAborted(request.signal);
787
819
  logProviderPayload("openai", "response payload", response, request.debug);
820
+ if (response.status && response.status !== "completed") {
821
+ throw Object.assign(
822
+ new Error(
823
+ response.error?.message ?? `Responses request ${response.status}`
824
+ ),
825
+ response.error
826
+ );
827
+ }
788
828
  return this.parseResponsesResult(response);
789
829
  }
790
830
  /**
@@ -802,13 +842,30 @@ var OpenAIAdapter = class _OpenAIAdapter {
802
842
  */
803
843
  async *streamWithResponses(request) {
804
844
  const client = await this.getClient();
845
+ throwIfAborted(request.signal);
805
846
  const payload = this.buildResponsesPayload(request, true);
806
847
  logProviderPayload("openai", "request payload", payload, request.debug);
807
- const stream = await client.responses.create(payload);
848
+ if (!client.responses?.create)
849
+ throw new Error(
850
+ "This OpenAI client does not support Responses; install an openai version with Responses support."
851
+ );
852
+ const stream = await client.responses.create(payload, {
853
+ signal: request.signal
854
+ });
808
855
  const toolCalls = /* @__PURE__ */ new Map();
809
856
  let usage;
810
857
  const citations = [];
858
+ let completed = false;
859
+ let terminalResponse;
860
+ let responseIdentity = {};
861
+ const outputItems = /* @__PURE__ */ new Map();
862
+ let streamedText = "";
811
863
  function* flushToolCalls() {
864
+ for (const call of toolCalls.values()) {
865
+ if (!call.name)
866
+ throw new Error("Responses tool call is missing its name");
867
+ JSON.parse(call.arguments || "{}");
868
+ }
812
869
  for (const call of toolCalls.values()) {
813
870
  yield { type: "action:start", id: call.id, name: call.name };
814
871
  yield { type: "action:args", id: call.id, args: call.arguments };
@@ -817,19 +874,38 @@ var OpenAIAdapter = class _OpenAIAdapter {
817
874
  toolCalls.clear();
818
875
  }
819
876
  for await (const event of stream) {
820
- if (request.signal?.aborted) break;
877
+ throwIfAborted(request.signal);
821
878
  logProviderPayload("openai", "stream chunk", event, request.debug);
879
+ if (completed)
880
+ throw new Error("Responses stream emitted an event after completion");
822
881
  const index = event?.output_index ?? 0;
823
882
  switch (event?.type) {
883
+ case "response.created":
884
+ responseIdentity = {
885
+ id: event.response?.id,
886
+ model: event.response?.model
887
+ };
888
+ break;
889
+ case "response.output_item.done":
890
+ if (event.item) outputItems.set(index, event.item);
891
+ if (event.item?.type === "function_call") {
892
+ toolCalls.set(index, {
893
+ id: event.item.call_id,
894
+ name: event.item.name,
895
+ arguments: event.item.arguments
896
+ });
897
+ }
898
+ break;
824
899
  case "response.output_text.delta":
825
900
  if (event.delta) {
901
+ streamedText += event.delta;
826
902
  yield { type: "message:delta", content: event.delta };
827
903
  }
828
904
  break;
829
905
  case "response.output_item.added":
830
906
  if (event.item?.type === "function_call") {
831
907
  toolCalls.set(index, {
832
- id: event.item.call_id ?? event.item.id ?? generateToolCallId(),
908
+ id: event.item.call_id ?? "",
833
909
  name: event.item.name ?? "",
834
910
  arguments: event.item.arguments ?? ""
835
911
  });
@@ -847,8 +923,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
847
923
  }
848
924
  break;
849
925
  }
850
- case "response.completed":
851
- case "response.incomplete": {
926
+ case "response.completed": {
927
+ if (event.response?.status && event.response.status !== "completed")
928
+ throw new Error(`Responses stream ${event.response.status}`);
929
+ completed = true;
930
+ terminalResponse = { ...responseIdentity, ...event.response };
852
931
  const found = this.collectResponsesCitations(
853
932
  event.response?.output,
854
933
  citations.length
@@ -864,21 +943,88 @@ var OpenAIAdapter = class _OpenAIAdapter {
864
943
  }
865
944
  break;
866
945
  }
867
- case "error":
868
- case "response.failed":
946
+ case "response.incomplete":
869
947
  throw new Error(
870
- event.message ?? event.response?.error?.message ?? "Responses stream failed"
948
+ `Responses stream incomplete: ${event.response?.incomplete_details?.reason ?? "unknown reason"}`
949
+ );
950
+ case "error":
951
+ case "response.failed": {
952
+ const details = event.response?.error ?? event;
953
+ throw Object.assign(
954
+ new Error(details.message ?? "Responses stream failed"),
955
+ {
956
+ code: details.code,
957
+ param: details.param,
958
+ status: details.status
959
+ }
871
960
  );
961
+ }
962
+ }
963
+ }
964
+ throwIfAborted(request.signal);
965
+ if (!completed)
966
+ throw new Error("Responses stream ended without a completed response");
967
+ const output = terminalResponse.output?.length ? terminalResponse.output : [...outputItems.entries()].sort(([a], [b]) => a - b).map(([, item]) => item);
968
+ if (output.some((item) => item.type === "function_call"))
969
+ toolCalls.clear();
970
+ for (const [index, item] of output.entries()) {
971
+ if (item.type === "function_call") {
972
+ toolCalls.set(index, {
973
+ id: item.call_id,
974
+ name: item.name,
975
+ arguments: item.arguments
976
+ });
872
977
  }
873
978
  }
979
+ const uniqueCalls = /* @__PURE__ */ new Map();
980
+ for (const call of toolCalls.values()) {
981
+ if (!call.id || !call.name)
982
+ throw new Error("Responses stream contains an incomplete tool call");
983
+ uniqueCalls.set(call.id, call);
984
+ }
985
+ toolCalls.clear();
986
+ [...uniqueCalls.values()].forEach(
987
+ (call, index) => toolCalls.set(index, call)
988
+ );
989
+ if (!output.length && toolCalls.size) {
990
+ for (const call of toolCalls.values())
991
+ output.push({
992
+ type: "function_call",
993
+ call_id: call.id,
994
+ name: call.name,
995
+ arguments: call.arguments
996
+ });
997
+ }
998
+ if (!output.length && streamedText)
999
+ output.push({
1000
+ type: "message",
1001
+ role: "assistant",
1002
+ content: [{ type: "output_text", text: streamedText, annotations: [] }]
1003
+ });
1004
+ terminalResponse.output = output;
1005
+ const finalText = this.parseResponsesResult(terminalResponse).content;
1006
+ if (finalText.startsWith(streamedText) && finalText.length > streamedText.length) {
1007
+ yield {
1008
+ type: "message:delta",
1009
+ content: finalText.slice(streamedText.length)
1010
+ };
1011
+ }
874
1012
  yield* flushToolCalls();
875
1013
  if (citations.length > 0) {
876
1014
  yield { type: "citation", citations: deduplicateCitations(citations) };
877
1015
  }
878
- yield { type: "message:end" };
879
- yield { type: "done", usage };
1016
+ yield {
1017
+ type: "message:end",
1018
+ providerMetadata: responseMetadata(terminalResponse)
1019
+ };
1020
+ yield {
1021
+ type: "done",
1022
+ usage,
1023
+ providerMetadata: responseMetadata(terminalResponse)
1024
+ };
880
1025
  }
881
1026
  async *stream(request) {
1027
+ throwIfAborted(request.signal);
882
1028
  if (this.shouldUseResponsesApi(request)) {
883
1029
  const messageId2 = generateMessageId();
884
1030
  yield { type: "message:start", id: messageId2 };
@@ -886,47 +1032,14 @@ var OpenAIAdapter = class _OpenAIAdapter {
886
1032
  yield* this.streamWithResponses(request);
887
1033
  return;
888
1034
  } catch (error) {
889
- yield {
890
- type: "error",
891
- message: error instanceof Error ? error.message : "Unknown error",
892
- code: "OPENAI_RESPONSES_ERROR"
893
- };
1035
+ yield providerErrorEvent(error, "OPENAI_RESPONSES_ERROR");
894
1036
  return;
895
1037
  }
896
1038
  }
897
1039
  const client = await this.getClient();
898
1040
  let messages;
899
1041
  if (request.rawMessages && request.rawMessages.length > 0) {
900
- const processedMessages = request.rawMessages.map((msg) => {
901
- if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0 && msg.content === "") {
902
- return { ...msg, content: null };
903
- }
904
- const hasAttachments = msg.attachments && Array.isArray(msg.attachments) && msg.attachments.length > 0;
905
- if (hasAttachments) {
906
- const content = [];
907
- if (msg.content) {
908
- content.push({ type: "text", text: msg.content });
909
- }
910
- for (const attachment of msg.attachments) {
911
- if (attachment.type === "image") {
912
- let imageUrl;
913
- if (attachment.url) {
914
- imageUrl = attachment.url;
915
- } else if (attachment.data) {
916
- imageUrl = attachment.data.startsWith("data:") ? attachment.data : `data:${attachment.mimeType || "image/png"};base64,${attachment.data}`;
917
- } else {
918
- continue;
919
- }
920
- content.push({
921
- type: "image_url",
922
- image_url: { url: imageUrl, detail: "auto" }
923
- });
924
- }
925
- }
926
- return { ...msg, content, attachments: void 0 };
927
- }
928
- return msg;
929
- });
1042
+ const processedMessages = this.formatRawMessages(request.rawMessages);
930
1043
  if (request.systemPrompt) {
931
1044
  const hasSystem = processedMessages.some((m) => m.role === "system");
932
1045
  if (!hasSystem) {
@@ -946,7 +1059,16 @@ var OpenAIAdapter = class _OpenAIAdapter {
946
1059
  request.systemPrompt
947
1060
  );
948
1061
  }
949
- const tools = request.actions?.length ? formatTools(request.actions) : [];
1062
+ const tools = request.toolDefinitions?.length ? request.toolDefinitions.filter((tool) => tool.available !== false).map((tool) => ({
1063
+ type: "function",
1064
+ function: {
1065
+ name: tool.name,
1066
+ description: tool.description,
1067
+ parameters: normalizeObjectJsonSchema(
1068
+ tool.inputSchema
1069
+ )
1070
+ }
1071
+ })) : formatTools(request.actions ?? []);
950
1072
  const webSearchConfig = request.webSearch ?? this.config.webSearch;
951
1073
  if (webSearchConfig) {
952
1074
  const webSearchTool = {
@@ -975,26 +1097,28 @@ var OpenAIAdapter = class _OpenAIAdapter {
975
1097
  tools: tools.length > 0 ? tools : void 0,
976
1098
  tool_choice: tools.length > 0 ? toolChoice : void 0,
977
1099
  parallel_tool_calls: tools.length > 0 ? openaiToolOptions?.parallelToolCalls : void 0,
978
- ...buildOpenAITokenParams(
1100
+ ...buildOpenAIParameters(
979
1101
  modelIdForPayload,
980
1102
  request.config?.maxTokens ?? this.config.maxTokens,
981
- request.config?.temperature ?? this.config.temperature
1103
+ request.config?.temperature ?? this.config.temperature,
1104
+ this.parameterOptions(request)
982
1105
  ),
983
1106
  response_format: toOpenAIResponseFormat(request.config?.responseFormat),
984
1107
  stream: true,
985
1108
  stream_options: { include_usage: true }
986
1109
  };
987
1110
  logProviderPayload("openai", "request payload", payload, request.debug);
988
- const stream = await client.chat.completions.create(payload);
989
- let currentToolCall = null;
1111
+ const stream = await client.chat.completions.create(payload, {
1112
+ signal: request.signal
1113
+ });
1114
+ const toolCalls = /* @__PURE__ */ new Map();
1115
+ let finishReason;
990
1116
  const collectedCitations = [];
991
1117
  let citationIndex = 0;
992
1118
  let usage;
993
1119
  for await (const chunk of stream) {
994
1120
  logProviderPayload("openai", "stream chunk", chunk, request.debug);
995
- if (request.signal?.aborted) {
996
- break;
997
- }
1121
+ throwIfAborted(request.signal);
998
1122
  const delta = chunk.choices[0]?.delta;
999
1123
  const choice = chunk.choices[0];
1000
1124
  if (delta?.content) {
@@ -1017,38 +1141,19 @@ var OpenAIAdapter = class _OpenAIAdapter {
1017
1141
  }
1018
1142
  }
1019
1143
  }
1020
- if (delta?.tool_calls) {
1021
- for (const toolCall of delta.tool_calls) {
1022
- if (toolCall.id) {
1023
- if (currentToolCall) {
1024
- yield {
1025
- type: "action:args",
1026
- id: currentToolCall.id,
1027
- args: currentToolCall.arguments
1028
- };
1029
- yield {
1030
- type: "action:end",
1031
- id: currentToolCall.id,
1032
- name: currentToolCall.name
1033
- };
1034
- }
1035
- const tcExtraContent = toolCall.extra_content;
1036
- currentToolCall = {
1037
- id: toolCall.id,
1038
- name: toolCall.function?.name || "",
1039
- arguments: toolCall.function?.arguments || "",
1040
- ...tcExtraContent ? { extra_content: tcExtraContent } : {}
1041
- };
1042
- yield {
1043
- type: "action:start",
1044
- id: currentToolCall.id,
1045
- name: currentToolCall.name,
1046
- ...currentToolCall.extra_content ? { extra_content: currentToolCall.extra_content } : {}
1047
- };
1048
- } else if (currentToolCall && toolCall.function?.arguments) {
1049
- currentToolCall.arguments += toolCall.function.arguments;
1050
- }
1051
- }
1144
+ for (const part of delta?.tool_calls ?? []) {
1145
+ const index = part.index ?? 0;
1146
+ const call = toolCalls.get(index) ?? {
1147
+ id: "",
1148
+ name: "",
1149
+ arguments: ""
1150
+ };
1151
+ if (part.id) call.id = part.id;
1152
+ if (part.function?.name) call.name += part.function.name;
1153
+ if (part.function?.arguments)
1154
+ call.arguments += part.function.arguments;
1155
+ if (part.extra_content) call.extra_content = part.extra_content;
1156
+ toolCalls.set(index, call);
1052
1157
  }
1053
1158
  if (chunk.usage) {
1054
1159
  usage = {
@@ -1057,21 +1162,28 @@ var OpenAIAdapter = class _OpenAIAdapter {
1057
1162
  total_tokens: chunk.usage.total_tokens
1058
1163
  };
1059
1164
  }
1060
- if (choice?.finish_reason) {
1061
- if (currentToolCall) {
1062
- yield {
1063
- type: "action:args",
1064
- id: currentToolCall.id,
1065
- args: currentToolCall.arguments
1066
- };
1067
- yield {
1068
- type: "action:end",
1069
- id: currentToolCall.id,
1070
- name: currentToolCall.name
1071
- };
1072
- currentToolCall = null;
1073
- }
1074
- }
1165
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
1166
+ }
1167
+ throwIfAborted(request.signal);
1168
+ if (!finishReason)
1169
+ throw new Error("OpenAI stream ended without a finish reason");
1170
+ if (toolCalls.size && !["tool_calls", "function_call"].includes(finishReason)) {
1171
+ throw new Error("OpenAI stream ended with incomplete tool calls");
1172
+ }
1173
+ for (const call of toolCalls.values()) {
1174
+ if (!call.id || !call.name)
1175
+ throw new Error("OpenAI stream contains an incomplete tool call");
1176
+ JSON.parse(call.arguments || "{}");
1177
+ }
1178
+ for (const call of toolCalls.values()) {
1179
+ yield {
1180
+ type: "action:start",
1181
+ id: call.id,
1182
+ name: call.name,
1183
+ ...call.extra_content ? { extra_content: call.extra_content } : {}
1184
+ };
1185
+ yield { type: "action:args", id: call.id, args: call.arguments };
1186
+ yield { type: "action:end", id: call.id, name: call.name };
1075
1187
  }
1076
1188
  if (collectedCitations.length > 0) {
1077
1189
  const uniqueCitations = deduplicateCitations(collectedCitations);
@@ -1080,26 +1192,18 @@ var OpenAIAdapter = class _OpenAIAdapter {
1080
1192
  yield { type: "message:end" };
1081
1193
  yield { type: "done", usage };
1082
1194
  } catch (error) {
1083
- yield {
1084
- type: "error",
1085
- message: error instanceof Error ? error.message : "Unknown error",
1086
- code: `${this.provider.toUpperCase()}_ERROR`
1087
- };
1195
+ yield providerErrorEvent(error, `${this.provider.toUpperCase()}_ERROR`);
1088
1196
  }
1089
1197
  }
1090
1198
  async complete(request) {
1199
+ throwIfAborted(request.signal);
1091
1200
  if (this.shouldUseResponsesApi(request)) {
1092
1201
  return this.completeWithResponses(request);
1093
1202
  }
1094
1203
  const client = await this.getClient();
1095
1204
  let messages;
1096
1205
  if (request.rawMessages && request.rawMessages.length > 0) {
1097
- const sanitized = request.rawMessages.map((msg) => {
1098
- if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0 && msg.content === "") {
1099
- return { ...msg, content: null };
1100
- }
1101
- return msg;
1102
- });
1206
+ const sanitized = this.formatRawMessages(request.rawMessages);
1103
1207
  if (request.systemPrompt && !sanitized.some((message2) => message2.role === "system")) {
1104
1208
  messages = [
1105
1209
  { role: "system", content: request.systemPrompt },
@@ -1114,7 +1218,16 @@ var OpenAIAdapter = class _OpenAIAdapter {
1114
1218
  request.systemPrompt
1115
1219
  );
1116
1220
  }
1117
- const tools = request.actions?.length ? formatTools(request.actions) : [];
1221
+ const tools = request.toolDefinitions?.length ? request.toolDefinitions.filter((tool) => tool.available !== false).map((tool) => ({
1222
+ type: "function",
1223
+ function: {
1224
+ name: tool.name,
1225
+ description: tool.description,
1226
+ parameters: normalizeObjectJsonSchema(
1227
+ tool.inputSchema
1228
+ )
1229
+ }
1230
+ })) : formatTools(request.actions ?? []);
1118
1231
  const openaiToolOptions = request.providerToolOptions?.openai;
1119
1232
  const toolChoice = openaiToolOptions?.toolChoice && typeof openaiToolOptions.toolChoice === "object" ? {
1120
1233
  type: "function",
@@ -1129,16 +1242,19 @@ var OpenAIAdapter = class _OpenAIAdapter {
1129
1242
  tools: tools.length > 0 ? tools : void 0,
1130
1243
  tool_choice: tools.length > 0 ? toolChoice : void 0,
1131
1244
  parallel_tool_calls: tools.length > 0 ? openaiToolOptions?.parallelToolCalls : void 0,
1132
- ...buildOpenAITokenParams(
1245
+ ...buildOpenAIParameters(
1133
1246
  modelIdForCompletePayload,
1134
1247
  request.config?.maxTokens ?? this.config.maxTokens,
1135
- request.config?.temperature ?? this.config.temperature
1248
+ request.config?.temperature ?? this.config.temperature,
1249
+ this.parameterOptions(request)
1136
1250
  ),
1137
1251
  response_format: toOpenAIResponseFormat(request.config?.responseFormat),
1138
1252
  stream: false
1139
1253
  };
1140
1254
  logProviderPayload("openai", "request payload", payload, request.debug);
1141
- const response = await client.chat.completions.create(payload);
1255
+ const response = await client.chat.completions.create(payload, {
1256
+ signal: request.signal
1257
+ });
1142
1258
  logProviderPayload("openai", "response payload", response, request.debug);
1143
1259
  const choice = response.choices?.[0];
1144
1260
  const message = choice?.message;
@@ -1188,6 +1304,411 @@ function createOpenAIAdapter(config) {
1188
1304
  return new OpenAIAdapter(config);
1189
1305
  }
1190
1306
 
1307
+ // src/providers/openai/provider.ts
1308
+ var OPENAI_MODELS = {
1309
+ // GPT-4o series
1310
+ "gpt-4o": { vision: true, tools: true, jsonMode: true, maxTokens: 128e3 },
1311
+ "gpt-5.2": {
1312
+ vision: true,
1313
+ tools: true,
1314
+ jsonMode: true,
1315
+ maxTokens: 128e3
1316
+ },
1317
+ "gpt-4o-2024-11-20": {
1318
+ vision: true,
1319
+ tools: true,
1320
+ jsonMode: true,
1321
+ maxTokens: 128e3
1322
+ },
1323
+ "gpt-4o-2024-08-06": {
1324
+ vision: true,
1325
+ tools: true,
1326
+ jsonMode: true,
1327
+ maxTokens: 128e3
1328
+ },
1329
+ // GPT-4 Turbo
1330
+ "gpt-4-turbo": {
1331
+ vision: true,
1332
+ tools: true,
1333
+ jsonMode: true,
1334
+ maxTokens: 128e3
1335
+ },
1336
+ "gpt-4-turbo-preview": {
1337
+ vision: false,
1338
+ tools: true,
1339
+ jsonMode: true,
1340
+ maxTokens: 128e3
1341
+ },
1342
+ // GPT-4
1343
+ "gpt-4": { vision: false, tools: true, jsonMode: false, maxTokens: 8192 },
1344
+ "gpt-4-32k": {
1345
+ vision: false,
1346
+ tools: true,
1347
+ jsonMode: false,
1348
+ maxTokens: 32768
1349
+ },
1350
+ // GPT-3.5
1351
+ "gpt-3.5-turbo": {
1352
+ vision: false,
1353
+ tools: true,
1354
+ jsonMode: true,
1355
+ maxTokens: 16385
1356
+ },
1357
+ // O1 series (reasoning)
1358
+ o1: { vision: true, tools: false, jsonMode: false, maxTokens: 128e3 },
1359
+ "o1-mini": { vision: true, tools: false, jsonMode: false, maxTokens: 128e3 },
1360
+ "o1-preview": {
1361
+ vision: true,
1362
+ tools: false,
1363
+ jsonMode: false,
1364
+ maxTokens: 128e3
1365
+ },
1366
+ // O3 series
1367
+ "o3-mini": { vision: true, tools: false, jsonMode: false, maxTokens: 128e3 }
1368
+ };
1369
+ function openai(modelId, options = {}) {
1370
+ const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
1371
+ const baseURL = options.baseURL ?? "https://api.openai.com/v1";
1372
+ let client = null;
1373
+ async function getClient() {
1374
+ if (!client) {
1375
+ const { default: OpenAI } = await import('openai');
1376
+ client = new OpenAI({
1377
+ apiKey,
1378
+ baseURL,
1379
+ organization: options.organization,
1380
+ defaultHeaders: options.headers
1381
+ });
1382
+ }
1383
+ return client;
1384
+ }
1385
+ function parameterOptions(params) {
1386
+ return {
1387
+ ...options,
1388
+ reasoningEffort: params.reasoningEffort ?? options.reasoningEffort
1389
+ };
1390
+ }
1391
+ function useResponses(params) {
1392
+ return useOpenAIResponses(
1393
+ modelId,
1394
+ baseURL,
1395
+ parameterOptions(params),
1396
+ resolveOpenAITransport(options, params.openai),
1397
+ !!params.tools?.length,
1398
+ params.messages.some(
1399
+ (message) => message.role === "assistant" && !!message.providerMetadata?.openai?.response
1400
+ )
1401
+ );
1402
+ }
1403
+ const responsesAdapter = new OpenAIAdapter({
1404
+ ...options,
1405
+ apiKey: apiKey ?? "",
1406
+ model: modelId,
1407
+ baseUrl: baseURL
1408
+ });
1409
+ function responsesRequest(params) {
1410
+ return {
1411
+ messages: [],
1412
+ rawMessages: formatMessagesForOpenAI2(params.messages, true),
1413
+ toolDefinitions: params.tools?.map((raw) => {
1414
+ const tool = raw;
1415
+ if (tool.type !== "function")
1416
+ throw new Error(
1417
+ "Only function tools are supported by this OpenAI generation path"
1418
+ );
1419
+ return {
1420
+ name: tool.function.name,
1421
+ description: tool.function.description ?? "",
1422
+ location: "server",
1423
+ inputSchema: tool.function.parameters
1424
+ };
1425
+ }),
1426
+ config: {
1427
+ openai: {
1428
+ ...resolveOpenAITransport(options, params.openai),
1429
+ api: "responses"
1430
+ },
1431
+ temperature: params.temperature,
1432
+ maxTokens: params.maxTokens,
1433
+ responseFormat: params.responseFormat,
1434
+ reasoningEffort: parameterOptions(params).reasoningEffort
1435
+ },
1436
+ signal: params.signal
1437
+ };
1438
+ }
1439
+ const modelConfig = OPENAI_MODELS[modelId] ?? OPENAI_MODELS["gpt-4o"];
1440
+ return {
1441
+ provider: "openai",
1442
+ modelId,
1443
+ capabilities: {
1444
+ supportsVision: modelConfig.vision,
1445
+ supportsTools: modelConfig.tools,
1446
+ supportsStreaming: true,
1447
+ supportsJsonMode: modelConfig.jsonMode,
1448
+ supportsThinking: false,
1449
+ supportsPDF: false,
1450
+ maxTokens: modelConfig.maxTokens,
1451
+ supportedImageTypes: modelConfig.vision ? ["image/png", "image/jpeg", "image/gif", "image/webp"] : []
1452
+ },
1453
+ async doGenerate(params) {
1454
+ throwIfAborted(params.signal);
1455
+ if (useResponses(params)) {
1456
+ const result = await responsesAdapter.complete(
1457
+ responsesRequest(params)
1458
+ );
1459
+ return {
1460
+ text: result.content,
1461
+ toolCalls: result.toolCalls,
1462
+ finishReason: result.toolCalls.length ? "tool-calls" : result.providerMetadata?.openai?.response?.refusals.length ? "content-filter" : "stop",
1463
+ usage: result.usage ?? {
1464
+ promptTokens: 0,
1465
+ completionTokens: 0,
1466
+ totalTokens: 0
1467
+ },
1468
+ rawResponse: result.rawResponse,
1469
+ providerMetadata: result.providerMetadata
1470
+ };
1471
+ }
1472
+ const client2 = await getClient();
1473
+ const messages = formatMessagesForOpenAI2(params.messages);
1474
+ const response = await client2.chat.completions.create(
1475
+ {
1476
+ model: modelId,
1477
+ messages,
1478
+ tools: params.tools,
1479
+ ...buildOpenAIParameters(
1480
+ modelId,
1481
+ params.maxTokens,
1482
+ params.temperature,
1483
+ parameterOptions(params)
1484
+ ),
1485
+ response_format: toOpenAIResponseFormat(params.responseFormat)
1486
+ },
1487
+ { signal: params.signal }
1488
+ );
1489
+ const choice = response.choices[0];
1490
+ const message = choice.message;
1491
+ const toolCalls = (message.tool_calls ?? []).map(
1492
+ (tc) => ({
1493
+ id: tc.id,
1494
+ name: tc.function.name,
1495
+ args: JSON.parse(tc.function.arguments || "{}")
1496
+ })
1497
+ );
1498
+ return {
1499
+ text: message.content ?? "",
1500
+ toolCalls,
1501
+ finishReason: mapFinishReason(choice.finish_reason),
1502
+ usage: {
1503
+ promptTokens: response.usage?.prompt_tokens ?? 0,
1504
+ completionTokens: response.usage?.completion_tokens ?? 0,
1505
+ totalTokens: response.usage?.total_tokens ?? 0
1506
+ },
1507
+ rawResponse: response
1508
+ };
1509
+ },
1510
+ async *doStream(params) {
1511
+ throwIfAborted(params.signal);
1512
+ if (useResponses(params)) {
1513
+ const calls2 = /* @__PURE__ */ new Map();
1514
+ let hasTools = false;
1515
+ for await (const event of responsesAdapter.stream(
1516
+ responsesRequest(params)
1517
+ )) {
1518
+ switch (event.type) {
1519
+ case "message:delta":
1520
+ yield { type: "text-delta", text: event.content };
1521
+ break;
1522
+ case "action:start":
1523
+ calls2.set(event.id, { name: event.name, args: "" });
1524
+ break;
1525
+ case "action:args": {
1526
+ const call = calls2.get(event.id);
1527
+ if (call) call.args += event.args;
1528
+ break;
1529
+ }
1530
+ case "action:end": {
1531
+ const call = calls2.get(event.id);
1532
+ if (!call)
1533
+ throw new Error("Responses tool call has no matching start");
1534
+ hasTools = true;
1535
+ yield {
1536
+ type: "tool-call",
1537
+ toolCall: {
1538
+ id: event.id,
1539
+ name: call.name,
1540
+ args: JSON.parse(call.args || "{}")
1541
+ }
1542
+ };
1543
+ calls2.delete(event.id);
1544
+ break;
1545
+ }
1546
+ case "error":
1547
+ throw errorFromEvent(event);
1548
+ case "done":
1549
+ yield {
1550
+ type: "finish",
1551
+ finishReason: hasTools ? "tool-calls" : event.providerMetadata?.openai?.response?.refusals.length ? "content-filter" : "stop",
1552
+ providerMetadata: event.providerMetadata,
1553
+ usage: {
1554
+ promptTokens: event.usage?.prompt_tokens ?? 0,
1555
+ completionTokens: event.usage?.completion_tokens ?? 0,
1556
+ totalTokens: event.usage?.total_tokens ?? 0
1557
+ }
1558
+ };
1559
+ break;
1560
+ }
1561
+ }
1562
+ return;
1563
+ }
1564
+ const client2 = await getClient();
1565
+ const messages = formatMessagesForOpenAI2(params.messages);
1566
+ const stream = await client2.chat.completions.create(
1567
+ {
1568
+ model: modelId,
1569
+ messages,
1570
+ tools: params.tools,
1571
+ ...buildOpenAIParameters(
1572
+ modelId,
1573
+ params.maxTokens,
1574
+ params.temperature,
1575
+ parameterOptions(params)
1576
+ ),
1577
+ response_format: toOpenAIResponseFormat(params.responseFormat),
1578
+ stream: true,
1579
+ stream_options: { include_usage: true }
1580
+ },
1581
+ { signal: params.signal }
1582
+ );
1583
+ const toolCalls = /* @__PURE__ */ new Map();
1584
+ let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1585
+ let finishReason;
1586
+ for await (const chunk of stream) {
1587
+ throwIfAborted(params.signal);
1588
+ if (chunk.usage) {
1589
+ usage = {
1590
+ promptTokens: chunk.usage.prompt_tokens ?? 0,
1591
+ completionTokens: chunk.usage.completion_tokens ?? 0,
1592
+ totalTokens: chunk.usage.total_tokens ?? (chunk.usage.prompt_tokens ?? 0) + (chunk.usage.completion_tokens ?? 0)
1593
+ };
1594
+ }
1595
+ const choice = chunk.choices?.[0];
1596
+ if (choice?.delta?.content)
1597
+ yield { type: "text-delta", text: choice.delta.content };
1598
+ for (const part of choice?.delta?.tool_calls ?? []) {
1599
+ const index = part.index ?? 0;
1600
+ const call = toolCalls.get(index) ?? {
1601
+ id: "",
1602
+ name: "",
1603
+ arguments: ""
1604
+ };
1605
+ if (part.id) call.id = part.id;
1606
+ if (part.function?.name) call.name += part.function.name;
1607
+ if (part.function?.arguments)
1608
+ call.arguments += part.function.arguments;
1609
+ toolCalls.set(index, call);
1610
+ }
1611
+ if (choice?.finish_reason)
1612
+ finishReason = mapFinishReason(choice.finish_reason);
1613
+ }
1614
+ throwIfAborted(params.signal);
1615
+ if (!finishReason)
1616
+ throw new Error("OpenAI stream ended without a finish reason");
1617
+ const calls = [...toolCalls.values()].map((call) => {
1618
+ if (!call.id || !call.name)
1619
+ throw new Error("OpenAI stream contains an incomplete tool call");
1620
+ return {
1621
+ id: call.id,
1622
+ name: call.name,
1623
+ args: JSON.parse(call.arguments || "{}")
1624
+ };
1625
+ });
1626
+ if (calls.length && finishReason !== "tool-calls")
1627
+ throw new Error("OpenAI stream ended with incomplete tool calls");
1628
+ for (const toolCall of calls) yield { type: "tool-call", toolCall };
1629
+ yield { type: "finish", finishReason, usage };
1630
+ }
1631
+ };
1632
+ }
1633
+ function mapFinishReason(reason) {
1634
+ switch (reason) {
1635
+ case "stop":
1636
+ return "stop";
1637
+ case "length":
1638
+ return "length";
1639
+ case "tool_calls":
1640
+ case "function_call":
1641
+ return "tool-calls";
1642
+ case "content_filter":
1643
+ return "content-filter";
1644
+ default:
1645
+ return "unknown";
1646
+ }
1647
+ }
1648
+ function formatMessagesForOpenAI2(messages, responses = false) {
1649
+ return messages.map((msg) => {
1650
+ switch (msg.role) {
1651
+ case "system":
1652
+ return { role: "system", content: msg.content };
1653
+ case "user":
1654
+ if (typeof msg.content === "string") {
1655
+ return { role: "user", content: msg.content };
1656
+ }
1657
+ return {
1658
+ role: "user",
1659
+ content: msg.content.map((part) => {
1660
+ if (part.type === "text") {
1661
+ return { type: "text", text: part.text };
1662
+ }
1663
+ if (part.type === "image") {
1664
+ const imageData = typeof part.image === "string" ? part.image : Buffer.from(part.image).toString("base64");
1665
+ const url = /^(data:|https?:\/\/)/.test(imageData) ? imageData : `data:${part.mimeType ?? "image/png"};base64,${imageData}`;
1666
+ return { type: "image_url", image_url: { url, detail: "auto" } };
1667
+ }
1668
+ if (part.type === "file") {
1669
+ if (!responses)
1670
+ throw new Error(
1671
+ "File inputs require the Responses API on this OpenAI path"
1672
+ );
1673
+ return /^https?:\/\//.test(part.data) ? { type: "input_file", file_url: part.data } : {
1674
+ type: "input_file",
1675
+ filename: part.filename ?? "document.pdf",
1676
+ file_data: part.data.startsWith("data:") ? part.data : `data:${part.mimeType};base64,${part.data}`
1677
+ };
1678
+ }
1679
+ throw new Error("Unsupported OpenAI message content");
1680
+ })
1681
+ };
1682
+ case "assistant":
1683
+ const assistantMsg = {
1684
+ role: "assistant",
1685
+ content: msg.content,
1686
+ ...responses && msg.providerMetadata ? { providerMetadata: msg.providerMetadata } : {}
1687
+ };
1688
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
1689
+ assistantMsg.tool_calls = msg.toolCalls.map((tc) => ({
1690
+ id: tc.id,
1691
+ type: "function",
1692
+ function: {
1693
+ name: tc.name,
1694
+ arguments: JSON.stringify(tc.args)
1695
+ }
1696
+ }));
1697
+ }
1698
+ return assistantMsg;
1699
+ case "tool":
1700
+ return {
1701
+ role: "tool",
1702
+ tool_call_id: msg.toolCallId,
1703
+ content: responses ? msg.content : textToolOutput(msg.modelOutput, msg.content),
1704
+ ...responses && msg.modelOutput ? { modelOutput: msg.modelOutput } : {}
1705
+ };
1706
+ default:
1707
+ return msg;
1708
+ }
1709
+ });
1710
+ }
1711
+
1191
1712
  // src/providers/types.ts
1192
1713
  function createCallableProvider(providerFn, properties) {
1193
1714
  Object.defineProperty(providerFn, "name", {
@@ -1204,6 +1725,394 @@ function createCallableProvider(providerFn, properties) {
1204
1725
  return providerFn;
1205
1726
  }
1206
1727
 
1728
+ // src/core/audio.ts
1729
+ var audioErrorMarker = /* @__PURE__ */ Symbol.for("@yourgpt/llm-sdk/AudioAPIError");
1730
+ var _a;
1731
+ var AudioAPIError = class extends Error {
1732
+ constructor(message, code, status, requestId) {
1733
+ super(message);
1734
+ this.code = code;
1735
+ this.status = status;
1736
+ this.requestId = requestId;
1737
+ this[_a] = true;
1738
+ this.name = "AudioAPIError";
1739
+ }
1740
+ static [(_a = audioErrorMarker, Symbol.hasInstance)](value) {
1741
+ return !!value && typeof value === "object" && value[audioErrorMarker] === true;
1742
+ }
1743
+ };
1744
+
1745
+ // src/providers/openai/audio.ts
1746
+ var formats = {
1747
+ mp3: "audio/mpeg",
1748
+ opus: "audio/ogg",
1749
+ aac: "audio/aac",
1750
+ flac: "audio/flac",
1751
+ wav: "audio/wav",
1752
+ pcm: "audio/pcm"
1753
+ };
1754
+ var extensions = {
1755
+ "audio/mpeg": "mp3",
1756
+ "audio/mp3": "mp3",
1757
+ "audio/mp4": "m4a",
1758
+ "video/mp4": "mp4",
1759
+ "audio/wav": "wav",
1760
+ "audio/x-wav": "wav",
1761
+ "audio/webm": "webm",
1762
+ "video/webm": "webm",
1763
+ "audio/ogg": "ogg",
1764
+ "audio/flac": "flac",
1765
+ "audio/x-flac": "flac"
1766
+ };
1767
+ var MAX_AUDIO = 25e6;
1768
+ function hasAudioHeader(data, format) {
1769
+ const head = Buffer.from(data.subarray(0, 12)).toString("latin1");
1770
+ if (format === "wav")
1771
+ return head.startsWith("RIFF") && head.slice(8, 12) === "WAVE";
1772
+ if (format === "mp3" || format === "mpeg" || format === "mpga")
1773
+ return head.startsWith("ID3") || data[0] === 255 && (data[1] & 224) === 224;
1774
+ if (format === "opus" || format === "ogg") return head.startsWith("OggS");
1775
+ if (format === "flac") return head.startsWith("fLaC");
1776
+ if (format === "aac") return data[0] === 255 && (data[1] & 246) === 240;
1777
+ if (format === "webm")
1778
+ return data[0] === 26 && data[1] === 69 && data[2] === 223 && data[3] === 163;
1779
+ if (format === "m4a" || format === "mp4") return head.slice(4, 8) === "ftyp";
1780
+ return format === "pcm" && data.length > 0 && data.length % 2 === 0;
1781
+ }
1782
+ function invalid(message) {
1783
+ throw new AudioAPIError(message, "invalid_input");
1784
+ }
1785
+ function unsupported(message) {
1786
+ throw new AudioAPIError(message, "unsupported_option");
1787
+ }
1788
+ function providerOptions(value, allowed) {
1789
+ for (const [provider, options] of Object.entries(value ?? {})) {
1790
+ if (provider !== "openai")
1791
+ unsupported(
1792
+ `Provider options for ${provider} cannot be used with OpenAI audio`
1793
+ );
1794
+ for (const key of Object.keys(options))
1795
+ if (!allowed.includes(key))
1796
+ unsupported(`Unsupported OpenAI audio option: ${key}`);
1797
+ }
1798
+ return value?.openai ?? {};
1799
+ }
1800
+ async function readBody(response, limit) {
1801
+ if (Number(response.headers.get("content-length")) > limit) {
1802
+ await response.body?.cancel();
1803
+ throw new AudioAPIError(
1804
+ "Audio API response exceeds the size limit",
1805
+ "invalid_response"
1806
+ );
1807
+ }
1808
+ const reader = response.body?.getReader();
1809
+ if (!reader) return new Uint8Array();
1810
+ const chunks = [];
1811
+ let length = 0;
1812
+ try {
1813
+ while (true) {
1814
+ const { done, value } = await reader.read();
1815
+ if (done) break;
1816
+ length += value.length;
1817
+ if (length > limit) {
1818
+ await reader.cancel();
1819
+ throw new AudioAPIError(
1820
+ "Audio API response exceeds the size limit",
1821
+ "invalid_response"
1822
+ );
1823
+ }
1824
+ chunks.push(value);
1825
+ }
1826
+ } finally {
1827
+ reader.releaseLock();
1828
+ }
1829
+ const data = new Uint8Array(length);
1830
+ let offset = 0;
1831
+ for (const chunk of chunks) {
1832
+ data.set(chunk, offset);
1833
+ offset += chunk.length;
1834
+ }
1835
+ return data;
1836
+ }
1837
+ function transport(config) {
1838
+ return async (path, body, signal) => {
1839
+ signal?.throwIfAborted();
1840
+ const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
1841
+ if (!apiKey) invalid("OpenAI audio requires an API key on the server");
1842
+ const headers = {
1843
+ Authorization: `Bearer ${apiKey}`
1844
+ };
1845
+ if (typeof body === "string") headers["Content-Type"] = "application/json";
1846
+ if (config.organization)
1847
+ headers["OpenAI-Organization"] = config.organization;
1848
+ if (config.project) headers["OpenAI-Project"] = config.project;
1849
+ let response;
1850
+ try {
1851
+ response = await (config.fetch ?? globalThis.fetch)(
1852
+ `${(config.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")}${path}`,
1853
+ { method: "POST", headers, body, signal, redirect: "error" }
1854
+ );
1855
+ } catch {
1856
+ signal?.throwIfAborted();
1857
+ throw new AudioAPIError("OpenAI audio request failed", "network_error");
1858
+ }
1859
+ if (!response.ok) {
1860
+ await response.body?.cancel();
1861
+ throw new AudioAPIError(
1862
+ `OpenAI audio request failed (HTTP ${response.status})`,
1863
+ "provider_error",
1864
+ response.status,
1865
+ response.headers.get("x-request-id") ?? void 0
1866
+ );
1867
+ }
1868
+ return response;
1869
+ };
1870
+ }
1871
+ function openaiSpeech(modelId = "gpt-4o-mini-tts", config = {}) {
1872
+ const request = transport(config);
1873
+ return {
1874
+ provider: "openai",
1875
+ modelId,
1876
+ async doGenerate(options) {
1877
+ const {
1878
+ text,
1879
+ voice = "alloy",
1880
+ outputFormat = "mp3",
1881
+ speed,
1882
+ instructions,
1883
+ signal
1884
+ } = options;
1885
+ signal?.throwIfAborted();
1886
+ providerOptions(options.providerOptions, []);
1887
+ if (typeof text !== "string" || !text.trim() || text.length > 4096)
1888
+ invalid("Speech text must contain 1\u20134096 characters");
1889
+ if (!Object.prototype.hasOwnProperty.call(formats, outputFormat))
1890
+ unsupported("Unsupported OpenAI speech output format");
1891
+ if (typeof voice !== "string" || !voice.trim())
1892
+ invalid("Speech voice must not be empty");
1893
+ if (speed !== void 0 && (!Number.isFinite(speed) || speed < 0.25 || speed > 4))
1894
+ invalid("Speech speed must be between 0.25 and 4");
1895
+ if (instructions !== void 0) {
1896
+ if (typeof instructions !== "string" || instructions.length > 4096)
1897
+ invalid(
1898
+ "Speech instructions must be a string of at most 4096 characters"
1899
+ );
1900
+ if (modelId === "tts-1" || modelId === "tts-1-hd")
1901
+ unsupported("Instructions are not supported by tts-1 or tts-1-hd");
1902
+ }
1903
+ const response = await request(
1904
+ "/audio/speech",
1905
+ JSON.stringify({
1906
+ model: modelId,
1907
+ input: text,
1908
+ voice,
1909
+ response_format: outputFormat,
1910
+ speed,
1911
+ instructions
1912
+ }),
1913
+ signal
1914
+ );
1915
+ const mediaType = response.headers.get("content-type")?.split(";")[0].trim();
1916
+ if (mediaType && !mediaType.startsWith("audio/") && mediaType !== "application/octet-stream") {
1917
+ await response.body?.cancel();
1918
+ throw new AudioAPIError(
1919
+ "Speech endpoint returned a non-audio response",
1920
+ "invalid_response"
1921
+ );
1922
+ }
1923
+ const data = await readBody(response, MAX_AUDIO);
1924
+ signal?.throwIfAborted();
1925
+ if (!data.length || !hasAudioHeader(data, outputFormat))
1926
+ throw new AudioAPIError(
1927
+ "Speech endpoint returned empty or invalid audio",
1928
+ "invalid_response"
1929
+ );
1930
+ return {
1931
+ audio: {
1932
+ uint8Array: data,
1933
+ get base64() {
1934
+ return Buffer.from(data).toString("base64");
1935
+ },
1936
+ mediaType: formats[outputFormat],
1937
+ format: outputFormat
1938
+ },
1939
+ warnings: [],
1940
+ response: {
1941
+ provider: "openai",
1942
+ modelId,
1943
+ requestId: response.headers.get("x-request-id") ?? void 0,
1944
+ status: response.status
1945
+ }
1946
+ };
1947
+ }
1948
+ };
1949
+ }
1950
+ async function audioBytes(options) {
1951
+ const blob = typeof Blob !== "undefined" && options.audio instanceof Blob ? options.audio : void 0;
1952
+ if (blob && blob.size > MAX_AUDIO)
1953
+ invalid("Transcription file exceeds 25 MB");
1954
+ const declared = options.mediaType?.split(";")[0].trim();
1955
+ const blobType = blob?.type.split(";")[0].trim();
1956
+ if (declared && blobType && declared !== blobType)
1957
+ invalid("Transcription mediaType does not match the Blob type");
1958
+ const mediaType = declared || blobType || "";
1959
+ const extension = extensions[mediaType];
1960
+ if (!extension)
1961
+ unsupported(
1962
+ "Unsupported transcription media type; provide an encoded audio file"
1963
+ );
1964
+ let bytes;
1965
+ if (blob) bytes = new Uint8Array(await blob.arrayBuffer());
1966
+ else if (options.audio instanceof Uint8Array) bytes = options.audio;
1967
+ else if (options.audio instanceof ArrayBuffer)
1968
+ bytes = new Uint8Array(options.audio);
1969
+ else if (typeof options.audio === "string") {
1970
+ const base64 = options.audio;
1971
+ if (base64.length > MAX_AUDIO * 4 / 3 + 4)
1972
+ invalid("Transcription file exceeds 25 MB");
1973
+ if (!base64 || base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64))
1974
+ invalid(
1975
+ "Audio must be bytes, a Blob, or valid base64; URLs are not fetched"
1976
+ );
1977
+ bytes = Buffer.from(base64, "base64");
1978
+ if (Buffer.from(bytes).toString("base64") !== base64)
1979
+ invalid("Invalid audio base64");
1980
+ } else invalid("Audio must be bytes, a Blob, or base64");
1981
+ if (!bytes.length || bytes.length > MAX_AUDIO)
1982
+ invalid("Transcription file must contain 1 byte to 25 MB");
1983
+ if (!hasAudioHeader(bytes, extension))
1984
+ invalid("Audio header does not match the declared media type");
1985
+ return { bytes, mediaType, extension };
1986
+ }
1987
+ function openaiTranscription(modelId = "gpt-4o-mini-transcribe", config = {}) {
1988
+ const request = transport(config);
1989
+ return {
1990
+ provider: "openai",
1991
+ modelId,
1992
+ async doTranscribe(options) {
1993
+ options.signal?.throwIfAborted();
1994
+ if (modelId.includes("realtime"))
1995
+ unsupported(
1996
+ "Realtime models require a realtime session; use a file transcription model"
1997
+ );
1998
+ const opts = providerOptions(options.providerOptions, [
1999
+ "timestampGranularities",
2000
+ "includeLogprobs",
2001
+ "chunkingStrategy"
2002
+ ]);
2003
+ const timestamps = opts.timestampGranularities;
2004
+ if (timestamps !== void 0 && (!Array.isArray(timestamps) || !timestamps.length || timestamps.some((x) => x !== "word" && x !== "segment")))
2005
+ invalid("timestampGranularities must contain word or segment");
2006
+ if (timestamps !== void 0 && modelId !== "whisper-1")
2007
+ unsupported("Timestamp granularities require whisper-1");
2008
+ if (opts.includeLogprobs !== void 0 && typeof opts.includeLogprobs !== "boolean")
2009
+ invalid("includeLogprobs must be boolean");
2010
+ if (opts.includeLogprobs && !/^gpt-4o-(mini-)?transcribe(?:-\d{4}-\d{2}-\d{2})?$/.test(modelId))
2011
+ unsupported(
2012
+ "Log probabilities require a supported gpt-4o transcription model"
2013
+ );
2014
+ const diarize = modelId === "gpt-4o-transcribe-diarize";
2015
+ if (opts.chunkingStrategy !== void 0 && opts.chunkingStrategy !== "auto")
2016
+ unsupported("Only automatic audio chunking is supported");
2017
+ if (diarize && options.prompt !== void 0)
2018
+ unsupported("Prompts are not supported by the diarization model");
2019
+ if (options.language !== void 0 && !/^[a-z]{2}$/.test(options.language))
2020
+ invalid("language must be a two-letter ISO-639-1 code");
2021
+ if (options.prompt !== void 0 && typeof options.prompt !== "string")
2022
+ invalid("Transcription prompt must be a string");
2023
+ const { bytes, mediaType, extension } = await audioBytes(options);
2024
+ options.signal?.throwIfAborted();
2025
+ const fileName = options.filename ?? `audio.${extension}`;
2026
+ if (!/^[^/\\\r\n\x00]+\.[a-z0-9]+$/i.test(fileName))
2027
+ invalid("Transcription filename must be a filename with an extension");
2028
+ const allowedExt = mediaType === "audio/mpeg" ? ["mp3", "mpeg", "mpga"] : mediaType === "audio/mp4" ? ["m4a", "mp4"] : [extension];
2029
+ if (!allowedExt.includes(fileName.split(".").at(-1).toLowerCase()))
2030
+ invalid("Transcription filename extension does not match mediaType");
2031
+ const form = new FormData();
2032
+ form.append(
2033
+ "file",
2034
+ new Blob([new Uint8Array(bytes)], { type: mediaType }),
2035
+ fileName
2036
+ );
2037
+ form.append("model", modelId);
2038
+ form.append(
2039
+ "response_format",
2040
+ diarize ? "diarized_json" : timestamps ? "verbose_json" : "json"
2041
+ );
2042
+ if (options.language) form.append("language", options.language);
2043
+ if (options.prompt) form.append("prompt", options.prompt);
2044
+ if (timestamps)
2045
+ for (const value2 of timestamps)
2046
+ form.append("timestamp_granularities[]", value2);
2047
+ if (opts.includeLogprobs) form.append("include[]", "logprobs");
2048
+ if (diarize || opts.chunkingStrategy)
2049
+ form.append("chunking_strategy", "auto");
2050
+ const response = await request(
2051
+ "/audio/transcriptions",
2052
+ form,
2053
+ options.signal
2054
+ );
2055
+ let value;
2056
+ try {
2057
+ value = JSON.parse(
2058
+ Buffer.from(await readBody(response, 5 * 1024 * 1024)).toString(
2059
+ "utf8"
2060
+ )
2061
+ );
2062
+ } catch (error) {
2063
+ options.signal?.throwIfAborted();
2064
+ if (error instanceof AudioAPIError) throw error;
2065
+ throw new AudioAPIError(
2066
+ "Transcription endpoint returned invalid JSON",
2067
+ "invalid_response"
2068
+ );
2069
+ }
2070
+ options.signal?.throwIfAborted();
2071
+ if (!value || typeof value.text !== "string")
2072
+ throw new AudioAPIError(
2073
+ "Transcription response is missing text",
2074
+ "invalid_response"
2075
+ );
2076
+ function timed(items, word = false) {
2077
+ if (items === void 0) return void 0;
2078
+ if (!Array.isArray(items) || items.some(
2079
+ (x) => !x || typeof x[word ? "word" : "text"] !== "string" || !Number.isFinite(x.start) || !Number.isFinite(x.end) || x.start < 0 || x.end < x.start
2080
+ ))
2081
+ throw new AudioAPIError(
2082
+ "Invalid transcription timestamps",
2083
+ "invalid_response"
2084
+ );
2085
+ return items.map(
2086
+ (x) => word ? { word: x.word, start: x.start, end: x.end } : {
2087
+ text: x.text,
2088
+ start: x.start,
2089
+ end: x.end,
2090
+ ...typeof x.speaker === "string" ? { speaker: x.speaker } : {}
2091
+ }
2092
+ );
2093
+ }
2094
+ return {
2095
+ text: value.text,
2096
+ language: typeof value.language === "string" ? value.language : void 0,
2097
+ durationSeconds: typeof value.duration === "number" ? value.duration : void 0,
2098
+ segments: timed(value.segments),
2099
+ words: timed(value.words, true),
2100
+ usage: value.usage,
2101
+ providerMetadata: {
2102
+ openai: { ...value.logprobs ? { logprobs: value.logprobs } : {} }
2103
+ },
2104
+ warnings: [],
2105
+ response: {
2106
+ provider: "openai",
2107
+ modelId,
2108
+ status: response.status,
2109
+ requestId: response.headers.get("x-request-id") ?? void 0
2110
+ }
2111
+ };
2112
+ }
2113
+ };
2114
+ }
2115
+
1207
2116
  // src/providers/openai/index.ts
1208
2117
  var OPENAI_MODELS2 = {
1209
2118
  // GPT-4o series
@@ -1318,7 +2227,12 @@ function createOpenAI(config = {}) {
1318
2227
  return createOpenAIAdapter({
1319
2228
  apiKey,
1320
2229
  model: modelId,
1321
- baseUrl: config.baseUrl
2230
+ baseUrl: config.baseUrl,
2231
+ reasoningEffort: config.reasoningEffort,
2232
+ api: config.api,
2233
+ responses: config.responses,
2234
+ parameterPolicy: config.parameterPolicy,
2235
+ onWarning: config.onWarning
1322
2236
  });
1323
2237
  };
1324
2238
  const getCapabilities = (modelId) => {
@@ -1346,6 +2260,6 @@ function createOpenAI(config = {}) {
1346
2260
  }
1347
2261
  var createOpenAIProvider = createOpenAI;
1348
2262
 
1349
- export { createOpenAI, openai as createOpenAIModel, createOpenAIProvider, openai };
2263
+ export { createOpenAI, openai as createOpenAIModel, createOpenAIProvider, openai, openaiSpeech, openaiTranscription };
1350
2264
  //# sourceMappingURL=index.mjs.map
1351
2265
  //# sourceMappingURL=index.mjs.map