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