llm-messages 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -10,7 +10,7 @@
10
10
  /** A supported provider. */
11
11
  type Provider = 'openai' | 'anthropic' | 'gemini';
12
12
  /** A stable, machine readable code describing a non fatal conversion event. */
13
- type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream';
13
+ type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream' | 'gemini-url-image';
14
14
  /** A non fatal event raised during conversion. */
15
15
  interface Warning {
16
16
  code: WarningCode;
@@ -25,8 +25,16 @@ interface OpenAITextPart {
25
25
  type: 'text';
26
26
  text: string;
27
27
  }
28
- /** A content part. Non text parts (images, audio) are preserved verbatim. */
29
- type OpenAIContentPart = OpenAITextPart | {
28
+ interface OpenAIImagePart {
29
+ type: 'image_url';
30
+ /** `url` is a remote https URL or a `data:<mediaType>;base64,<data>` data URL. */
31
+ image_url: {
32
+ url: string;
33
+ detail?: 'auto' | 'low' | 'high' | 'original';
34
+ };
35
+ }
36
+ /** A content part. Unknown part types (audio, files) are preserved verbatim. */
37
+ type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | {
30
38
  type: string;
31
39
  [key: string]: unknown;
32
40
  };
@@ -80,7 +88,18 @@ interface AnthropicToolResultBlock {
80
88
  content: string | AnthropicTextBlock[];
81
89
  is_error?: boolean;
82
90
  }
83
- type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | {
91
+ interface AnthropicImageBlock {
92
+ type: 'image';
93
+ source: {
94
+ type: 'base64';
95
+ media_type: string;
96
+ data: string;
97
+ } | {
98
+ type: 'url';
99
+ url: string;
100
+ };
101
+ }
102
+ type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | {
84
103
  type: string;
85
104
  [key: string]: unknown;
86
105
  };
@@ -112,7 +131,19 @@ interface GeminiFunctionResponsePart {
112
131
  response: Record<string, unknown>;
113
132
  };
114
133
  }
115
- type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart | Record<string, unknown>;
134
+ interface GeminiInlineDataPart {
135
+ inlineData: {
136
+ mimeType: string;
137
+ data: string;
138
+ };
139
+ }
140
+ interface GeminiFileDataPart {
141
+ fileData: {
142
+ mimeType?: string;
143
+ fileUri: string;
144
+ };
145
+ }
146
+ type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart | GeminiInlineDataPart | GeminiFileDataPart | Record<string, unknown>;
116
147
  interface GeminiContent {
117
148
  /** `model` is Gemini's name for the assistant role. */
118
149
  role?: 'user' | 'model';
@@ -176,4 +207,49 @@ declare function convert<From extends Provider, To extends Provider>(conversatio
176
207
  to: To;
177
208
  }, options?: ConvertOptions): ConversationOf<To>;
178
209
 
179
- export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type GeminiContent, type GeminiConversation, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiPart, type GeminiTextPart, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, toAnthropic, toGemini };
210
+ /**
211
+ * A provider-neutral image. `base64` carries the raw bytes plus media type
212
+ * (mapping to a data URL, an Anthropic base64 source or a Gemini `inlineData`
213
+ * part); `url` carries a remote reference.
214
+ */
215
+ type NormalizedImage = {
216
+ kind: 'base64';
217
+ mediaType: string;
218
+ data: string;
219
+ } | {
220
+ kind: 'url';
221
+ url: string;
222
+ };
223
+ /** Decomposes a `data:<mediaType>;base64,<data>` URL. Returns null otherwise. */
224
+ declare function parseDataUrl(url: string): {
225
+ mediaType: string;
226
+ data: string;
227
+ } | null;
228
+ /** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */
229
+ declare function toDataUrl(mediaType: string, data: string): string;
230
+
231
+ /** A provider-neutral finish reason. */
232
+ type FinishReason = 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'unknown';
233
+ /** Provider-neutral token usage. */
234
+ interface Usage {
235
+ inputTokens: number;
236
+ outputTokens: number;
237
+ }
238
+ /** A provider response normalized to the canonical OpenAI assistant shape. */
239
+ interface NormalizedResponse {
240
+ message: OpenAIAssistantMessage;
241
+ finishReason: FinishReason;
242
+ usage: Usage;
243
+ }
244
+ /** Normalizes an OpenAI Chat Completions response body. */
245
+ declare function responseFromOpenAI(body: unknown): NormalizedResponse;
246
+ /** Normalizes an Anthropic Messages response body. */
247
+ declare function responseFromAnthropic(body: unknown): NormalizedResponse;
248
+ /** Normalizes a Gemini generateContent response body. */
249
+ declare function responseFromGemini(body: unknown, options?: ConvertOptions): NormalizedResponse;
250
+ /** Normalizes a provider response body into the canonical shape. */
251
+ declare function normalizeResponse(body: unknown, route: {
252
+ from: Provider;
253
+ }, options?: ConvertOptions): NormalizedResponse;
254
+
255
+ export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  /** A supported provider. */
11
11
  type Provider = 'openai' | 'anthropic' | 'gemini';
12
12
  /** A stable, machine readable code describing a non fatal conversion event. */
13
- type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream';
13
+ type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream' | 'gemini-url-image';
14
14
  /** A non fatal event raised during conversion. */
15
15
  interface Warning {
16
16
  code: WarningCode;
@@ -25,8 +25,16 @@ interface OpenAITextPart {
25
25
  type: 'text';
26
26
  text: string;
27
27
  }
28
- /** A content part. Non text parts (images, audio) are preserved verbatim. */
29
- type OpenAIContentPart = OpenAITextPart | {
28
+ interface OpenAIImagePart {
29
+ type: 'image_url';
30
+ /** `url` is a remote https URL or a `data:<mediaType>;base64,<data>` data URL. */
31
+ image_url: {
32
+ url: string;
33
+ detail?: 'auto' | 'low' | 'high' | 'original';
34
+ };
35
+ }
36
+ /** A content part. Unknown part types (audio, files) are preserved verbatim. */
37
+ type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | {
30
38
  type: string;
31
39
  [key: string]: unknown;
32
40
  };
@@ -80,7 +88,18 @@ interface AnthropicToolResultBlock {
80
88
  content: string | AnthropicTextBlock[];
81
89
  is_error?: boolean;
82
90
  }
83
- type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | {
91
+ interface AnthropicImageBlock {
92
+ type: 'image';
93
+ source: {
94
+ type: 'base64';
95
+ media_type: string;
96
+ data: string;
97
+ } | {
98
+ type: 'url';
99
+ url: string;
100
+ };
101
+ }
102
+ type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | {
84
103
  type: string;
85
104
  [key: string]: unknown;
86
105
  };
@@ -112,7 +131,19 @@ interface GeminiFunctionResponsePart {
112
131
  response: Record<string, unknown>;
113
132
  };
114
133
  }
115
- type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart | Record<string, unknown>;
134
+ interface GeminiInlineDataPart {
135
+ inlineData: {
136
+ mimeType: string;
137
+ data: string;
138
+ };
139
+ }
140
+ interface GeminiFileDataPart {
141
+ fileData: {
142
+ mimeType?: string;
143
+ fileUri: string;
144
+ };
145
+ }
146
+ type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart | GeminiInlineDataPart | GeminiFileDataPart | Record<string, unknown>;
116
147
  interface GeminiContent {
117
148
  /** `model` is Gemini's name for the assistant role. */
118
149
  role?: 'user' | 'model';
@@ -176,4 +207,49 @@ declare function convert<From extends Provider, To extends Provider>(conversatio
176
207
  to: To;
177
208
  }, options?: ConvertOptions): ConversationOf<To>;
178
209
 
179
- export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type GeminiContent, type GeminiConversation, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiPart, type GeminiTextPart, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, toAnthropic, toGemini };
210
+ /**
211
+ * A provider-neutral image. `base64` carries the raw bytes plus media type
212
+ * (mapping to a data URL, an Anthropic base64 source or a Gemini `inlineData`
213
+ * part); `url` carries a remote reference.
214
+ */
215
+ type NormalizedImage = {
216
+ kind: 'base64';
217
+ mediaType: string;
218
+ data: string;
219
+ } | {
220
+ kind: 'url';
221
+ url: string;
222
+ };
223
+ /** Decomposes a `data:<mediaType>;base64,<data>` URL. Returns null otherwise. */
224
+ declare function parseDataUrl(url: string): {
225
+ mediaType: string;
226
+ data: string;
227
+ } | null;
228
+ /** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */
229
+ declare function toDataUrl(mediaType: string, data: string): string;
230
+
231
+ /** A provider-neutral finish reason. */
232
+ type FinishReason = 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'unknown';
233
+ /** Provider-neutral token usage. */
234
+ interface Usage {
235
+ inputTokens: number;
236
+ outputTokens: number;
237
+ }
238
+ /** A provider response normalized to the canonical OpenAI assistant shape. */
239
+ interface NormalizedResponse {
240
+ message: OpenAIAssistantMessage;
241
+ finishReason: FinishReason;
242
+ usage: Usage;
243
+ }
244
+ /** Normalizes an OpenAI Chat Completions response body. */
245
+ declare function responseFromOpenAI(body: unknown): NormalizedResponse;
246
+ /** Normalizes an Anthropic Messages response body. */
247
+ declare function responseFromAnthropic(body: unknown): NormalizedResponse;
248
+ /** Normalizes a Gemini generateContent response body. */
249
+ declare function responseFromGemini(body: unknown, options?: ConvertOptions): NormalizedResponse;
250
+ /** Normalizes a provider response body into the canonical shape. */
251
+ declare function normalizeResponse(body: unknown, route: {
252
+ from: Provider;
253
+ }, options?: ConvertOptions): NormalizedResponse;
254
+
255
+ export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
package/dist/index.js CHANGED
@@ -51,6 +51,68 @@ function unwrapResponse(response) {
51
51
  return JSON.stringify(response);
52
52
  }
53
53
 
54
+ // src/image.ts
55
+ var DATA_URL = /^data:([^;,]+);base64,(.*)$/s;
56
+ function parseDataUrl(url) {
57
+ const match = DATA_URL.exec(url);
58
+ if (!match) return null;
59
+ return { mediaType: match[1], data: match[2] };
60
+ }
61
+ function toDataUrl(mediaType, data) {
62
+ return `data:${mediaType};base64,${data}`;
63
+ }
64
+ function imageFromOpenAI(part) {
65
+ if (!isRecord(part) || part.type !== "image_url" || !isRecord(part.image_url)) return null;
66
+ const url = part.image_url.url;
67
+ if (typeof url !== "string") return null;
68
+ const parsed = parseDataUrl(url);
69
+ return parsed ? { kind: "base64", ...parsed } : { kind: "url", url };
70
+ }
71
+ function imageToOpenAI(image) {
72
+ const url = image.kind === "base64" ? toDataUrl(image.mediaType, image.data) : image.url;
73
+ return { type: "image_url", image_url: { url } };
74
+ }
75
+ function imageFromAnthropic(block) {
76
+ if (!isRecord(block) || block.type !== "image" || !isRecord(block.source)) return null;
77
+ const source = block.source;
78
+ if (source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string") {
79
+ return { kind: "base64", mediaType: source.media_type, data: source.data };
80
+ }
81
+ if (source.type === "url" && typeof source.url === "string") {
82
+ return { kind: "url", url: source.url };
83
+ }
84
+ return null;
85
+ }
86
+ function imageToAnthropic(image) {
87
+ if (image.kind === "base64") {
88
+ return { type: "image", source: { type: "base64", media_type: image.mediaType, data: image.data } };
89
+ }
90
+ return { type: "image", source: { type: "url", url: image.url } };
91
+ }
92
+ function imageFromGemini(part) {
93
+ if (isRecord(part) && isRecord(part.inlineData)) {
94
+ const data = part.inlineData;
95
+ if (typeof data.mimeType === "string" && typeof data.data === "string") {
96
+ return { kind: "base64", mediaType: data.mimeType, data: data.data };
97
+ }
98
+ }
99
+ if (isRecord(part) && isRecord(part.fileData)) {
100
+ const data = part.fileData;
101
+ if (typeof data.fileUri === "string") return { kind: "url", url: data.fileUri };
102
+ }
103
+ return null;
104
+ }
105
+ function imageToGemini(image, reporter) {
106
+ if (image.kind === "base64") {
107
+ return { inlineData: { mimeType: image.mediaType, data: image.data } };
108
+ }
109
+ reporter.warn(
110
+ "gemini-url-image",
111
+ "A remote image URL was emitted as Gemini fileData.fileUri; Gemini may require the Files API for non-Google URIs."
112
+ );
113
+ return { fileData: { fileUri: image.url } };
114
+ }
115
+
54
116
  // src/providers/openai.ts
55
117
  function isSystem(message) {
56
118
  return message.role === "system" || message.role === "developer";
@@ -112,9 +174,14 @@ function userContent(content, reporter) {
112
174
  for (const part of content) {
113
175
  if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
114
176
  blocks.push({ type: "text", text: part.text });
115
- } else {
116
- reporter.warn("dropped-content", "Dropped a non-text user content part not supported by this converter.");
177
+ continue;
178
+ }
179
+ const image = imageFromOpenAI(part);
180
+ if (image) {
181
+ blocks.push(imageToAnthropic(image));
182
+ continue;
117
183
  }
184
+ reporter.warn("dropped-content", "Dropped an unsupported user content part.");
118
185
  }
119
186
  return blocks;
120
187
  }
@@ -164,7 +231,7 @@ function fromAnthropic(conversation, options = {}) {
164
231
  const blocks = asBlocks(message.content);
165
232
  if (message.role === "user") {
166
233
  const toolResults = blocks.filter((b) => b.type === "tool_result");
167
- const textBlocks = blocks.filter((b) => b.type !== "tool_result");
234
+ const contentBlocks = blocks.filter((b) => b.type !== "tool_result");
168
235
  for (const block of toolResults) {
169
236
  out.push({
170
237
  role: "tool",
@@ -172,8 +239,8 @@ function fromAnthropic(conversation, options = {}) {
172
239
  content: textOf(block.content)
173
240
  });
174
241
  }
175
- if (textBlocks.length > 0) {
176
- out.push({ role: "user", content: textOf(textBlocks) });
242
+ if (contentBlocks.length > 0) {
243
+ out.push({ role: "user", content: userContentToOpenAI(contentBlocks) });
177
244
  }
178
245
  continue;
179
246
  }
@@ -190,6 +257,20 @@ function fromAnthropic(conversation, options = {}) {
190
257
  }
191
258
  return out;
192
259
  }
260
+ function userContentToOpenAI(blocks) {
261
+ const hasImage = blocks.some((block) => imageFromAnthropic(block) !== null);
262
+ if (!hasImage) return textOf(blocks);
263
+ const parts = [];
264
+ for (const block of blocks) {
265
+ const image = imageFromAnthropic(block);
266
+ if (image) {
267
+ parts.push(imageToOpenAI(image));
268
+ } else if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
269
+ parts.push({ type: "text", text: block.text });
270
+ }
271
+ }
272
+ return parts;
273
+ }
193
274
 
194
275
  // src/providers/gemini.ts
195
276
  function toGemini(messages, options = {}) {
@@ -244,9 +325,14 @@ function userParts(content, reporter) {
244
325
  for (const part of content) {
245
326
  if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
246
327
  parts.push({ text: part.text });
247
- } else {
248
- reporter.warn("dropped-content", "Dropped a non-text user content part not supported by this converter.");
328
+ continue;
249
329
  }
330
+ const image = imageFromOpenAI(part);
331
+ if (image) {
332
+ parts.push(imageToGemini(image, reporter));
333
+ continue;
334
+ }
335
+ reporter.warn("dropped-content", "Dropped an unsupported user content part.");
250
336
  }
251
337
  return parts.length > 0 ? parts : [{ text: "" }];
252
338
  }
@@ -291,7 +377,7 @@ function fromGemini(conversation, options = {}) {
291
377
  for (const content of conversation.contents) {
292
378
  const parts = Array.isArray(content.parts) ? content.parts : [];
293
379
  if (content.role === "model") {
294
- const textPieces2 = [];
380
+ const textPieces = [];
295
381
  const toolCalls = [];
296
382
  for (const part of parts) {
297
383
  if (isRecord(part) && isRecord(part.functionCall)) {
@@ -305,27 +391,42 @@ function fromGemini(conversation, options = {}) {
305
391
  });
306
392
  pending.push({ id, name: fc.name });
307
393
  } else if (isRecord(part) && typeof part.text === "string") {
308
- textPieces2.push(part.text);
394
+ textPieces.push(part.text);
309
395
  }
310
396
  }
311
- const text2 = textPieces2.join("");
312
- const assistant = { role: "assistant", content: text2 || null };
397
+ const text = textPieces.join("");
398
+ const assistant = { role: "assistant", content: text || null };
313
399
  if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
314
400
  out.push(assistant);
315
401
  continue;
316
402
  }
317
- const textPieces = [];
403
+ const contentParts = [];
404
+ let hasImage = false;
318
405
  for (const part of parts) {
319
406
  if (isRecord(part) && isRecord(part.functionResponse)) {
320
407
  const fr = part.functionResponse;
321
408
  const id = resolveResponseId(fr, pending, reporter, generateId);
322
409
  out.push({ role: "tool", tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });
323
- } else if (isRecord(part) && typeof part.text === "string") {
324
- textPieces.push(part.text);
410
+ continue;
411
+ }
412
+ const image = imageFromGemini(part);
413
+ if (image) {
414
+ contentParts.push(imageToOpenAI(image));
415
+ hasImage = true;
416
+ continue;
417
+ }
418
+ if (isRecord(part) && typeof part.text === "string") {
419
+ contentParts.push({ type: "text", text: part.text });
420
+ }
421
+ }
422
+ if (contentParts.length > 0) {
423
+ if (hasImage) {
424
+ out.push({ role: "user", content: contentParts });
425
+ } else {
426
+ const text = textOf(contentParts);
427
+ if (text) out.push({ role: "user", content: text });
325
428
  }
326
429
  }
327
- const text = textPieces.join("");
328
- if (text) out.push({ role: "user", content: text });
329
430
  }
330
431
  return out;
331
432
  }
@@ -378,11 +479,130 @@ function fromCanonical(canonical, to, options) {
378
479
  throw new Error(`Unknown target provider: ${String(to)}`);
379
480
  }
380
481
  }
482
+
483
+ // src/response.ts
484
+ var num = (value) => typeof value === "number" ? value : 0;
485
+ function buildMessage(text, toolCalls) {
486
+ const message = { role: "assistant", content: text ? text : null };
487
+ if (toolCalls.length > 0) message.tool_calls = toolCalls;
488
+ return message;
489
+ }
490
+ function finalReason(mapped, toolCalls) {
491
+ return toolCalls.length > 0 ? "tool_calls" : mapped;
492
+ }
493
+ var OPENAI_FINISH = {
494
+ stop: "stop",
495
+ length: "length",
496
+ tool_calls: "tool_calls",
497
+ content_filter: "content_filter",
498
+ function_call: "tool_calls"
499
+ };
500
+ function responseFromOpenAI(body) {
501
+ const root = isRecord(body) ? body : {};
502
+ const choice = Array.isArray(root.choices) && isRecord(root.choices[0]) ? root.choices[0] : {};
503
+ const message = isRecord(choice.message) ? choice.message : {};
504
+ const text = typeof message.content === "string" ? message.content : textOf(message.content);
505
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
506
+ const usage = isRecord(root.usage) ? root.usage : {};
507
+ return {
508
+ message: buildMessage(text, toolCalls),
509
+ finishReason: finalReason(OPENAI_FINISH[String(choice.finish_reason)] ?? "unknown", toolCalls),
510
+ usage: { inputTokens: num(usage.prompt_tokens), outputTokens: num(usage.completion_tokens) }
511
+ };
512
+ }
513
+ var ANTHROPIC_FINISH = {
514
+ end_turn: "stop",
515
+ stop_sequence: "stop",
516
+ tool_use: "tool_calls",
517
+ max_tokens: "length",
518
+ refusal: "content_filter",
519
+ pause_turn: "unknown"
520
+ };
521
+ function responseFromAnthropic(body) {
522
+ const root = isRecord(body) ? body : {};
523
+ const blocks = Array.isArray(root.content) ? root.content : [];
524
+ const textPieces = [];
525
+ const toolCalls = [];
526
+ for (const block of blocks) {
527
+ if (!isRecord(block)) continue;
528
+ if (block.type === "text" && typeof block.text === "string") {
529
+ textPieces.push(block.text);
530
+ } else if (block.type === "tool_use" && typeof block.name === "string") {
531
+ toolCalls.push({
532
+ id: typeof block.id === "string" ? block.id : "",
533
+ type: "function",
534
+ function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) }
535
+ });
536
+ }
537
+ }
538
+ const usage = isRecord(root.usage) ? root.usage : {};
539
+ return {
540
+ message: buildMessage(textPieces.join(""), toolCalls),
541
+ finishReason: finalReason(ANTHROPIC_FINISH[String(root.stop_reason)] ?? "unknown", toolCalls),
542
+ usage: { inputTokens: num(usage.input_tokens), outputTokens: num(usage.output_tokens) }
543
+ };
544
+ }
545
+ var GEMINI_FINISH = {
546
+ STOP: "stop",
547
+ MAX_TOKENS: "length",
548
+ SAFETY: "content_filter",
549
+ RECITATION: "content_filter",
550
+ MALFORMED_FUNCTION_CALL: "content_filter"
551
+ };
552
+ function responseFromGemini(body, options = {}) {
553
+ const reporter = new Reporter(options);
554
+ const root = isRecord(body) ? body : {};
555
+ const candidate = Array.isArray(root.candidates) && isRecord(root.candidates[0]) ? root.candidates[0] : {};
556
+ const content = isRecord(candidate.content) ? candidate.content : {};
557
+ const parts = Array.isArray(content.parts) ? content.parts : [];
558
+ const textPieces = [];
559
+ const toolCalls = [];
560
+ let counter = 0;
561
+ for (const part of parts) {
562
+ if (!isRecord(part)) continue;
563
+ if (isRecord(part.functionCall)) {
564
+ const call = part.functionCall;
565
+ const name = typeof call.name === "string" ? call.name : "function";
566
+ let id = call.id;
567
+ if (!id) {
568
+ id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
569
+ reporter.warn("generated-id", `Gemini functionCall '${name}' had no id; generated '${id}'.`);
570
+ }
571
+ toolCalls.push({ id, type: "function", function: { name, arguments: JSON.stringify(call.args ?? {}) } });
572
+ } else if (typeof part.text === "string") {
573
+ textPieces.push(part.text);
574
+ }
575
+ }
576
+ const usage = isRecord(root.usageMetadata) ? root.usageMetadata : {};
577
+ return {
578
+ message: buildMessage(textPieces.join(""), toolCalls),
579
+ finishReason: finalReason(GEMINI_FINISH[String(candidate.finishReason)] ?? "unknown", toolCalls),
580
+ usage: { inputTokens: num(usage.promptTokenCount), outputTokens: num(usage.candidatesTokenCount) }
581
+ };
582
+ }
583
+ function normalizeResponse(body, route, options = {}) {
584
+ switch (route.from) {
585
+ case "openai":
586
+ return responseFromOpenAI(body);
587
+ case "anthropic":
588
+ return responseFromAnthropic(body);
589
+ case "gemini":
590
+ return responseFromGemini(body, options);
591
+ default:
592
+ throw new Error(`Unknown source provider: ${String(route.from)}`);
593
+ }
594
+ }
381
595
  export {
382
596
  convert,
383
597
  fromAnthropic,
384
598
  fromGemini,
599
+ normalizeResponse,
600
+ parseDataUrl,
601
+ responseFromAnthropic,
602
+ responseFromGemini,
603
+ responseFromOpenAI,
385
604
  toAnthropic,
605
+ toDataUrl,
386
606
  toGemini
387
607
  };
388
608
  //# sourceMappingURL=index.js.map