llm-messages 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
5
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.0] - 2026-06-01
8
+
9
+ ### Added
10
+
11
+ - Multimodal image support. Image parts convert across all three providers:
12
+ OpenAI `image_url` (data URL or remote), Anthropic `image` blocks (base64 or
13
+ url source), and Gemini `inlineData` (base64) or `fileData` (remote). Base64
14
+ data URLs round trip losslessly.
15
+ - `parseDataUrl` / `toDataUrl` helpers and a `NormalizedImage` type.
16
+
17
+ ### Notes
18
+
19
+ - A remote image URL sent to Gemini is emitted as `fileData.fileUri` and flagged
20
+ with a `gemini-url-image` warning, since Gemini may require the Files API for
21
+ non-Google URIs.
22
+
7
23
  ## [0.1.0] - 2026-06-01
8
24
 
9
25
  ### Added
package/README.md CHANGED
@@ -114,12 +114,35 @@ Warning codes: `generated-id`, `unmapped-tool-result`, `merged-role`,
114
114
  | Match key | `tool_call_id` | `tool_use_id` | function `name` (id optional) |
115
115
  | Role alternation | not required | strict | strict |
116
116
 
117
+ ## Images
118
+
119
+ Image parts convert across all three providers:
120
+
121
+ ```ts
122
+ const messages = [
123
+ {
124
+ role: 'user',
125
+ content: [
126
+ { type: 'text', text: 'What is in this image?' },
127
+ { type: 'image_url', image_url: { url: 'data:image/png;base64,iVBORw0KGgo...' } },
128
+ ],
129
+ },
130
+ ];
131
+
132
+ toAnthropic(messages); // -> { type: 'image', source: { type: 'base64', media_type: 'image/png', data: '...' } }
133
+ toGemini(messages); // -> { inlineData: { mimeType: 'image/png', data: '...' } }
134
+ ```
135
+
136
+ Base64 data URLs round trip losslessly. A remote `https` URL maps to an Anthropic
137
+ `url` source; for Gemini it is emitted as `fileData.fileUri` with a
138
+ `gemini-url-image` warning, since Gemini may require the Files API for non-Google
139
+ URIs.
140
+
117
141
  ## Scope
118
142
 
119
- Version 0.x covers text, system prompts, and tool calls/results, which is the
120
- core of every agent loop. Multimodal parts (images, audio) are passed through
121
- where possible and reported as `dropped-content` otherwise; first-class
122
- multimodal mapping is on the roadmap.
143
+ Version 0.x covers text, system prompts, tool calls/results and images, which is
144
+ the core of every agent loop. Other modalities (audio, files) are passed through
145
+ where possible and reported as `dropped-content` otherwise.
123
146
 
124
147
  ## Part of a set
125
148
 
package/dist/index.cjs CHANGED
@@ -23,7 +23,9 @@ __export(index_exports, {
23
23
  convert: () => convert,
24
24
  fromAnthropic: () => fromAnthropic,
25
25
  fromGemini: () => fromGemini,
26
+ parseDataUrl: () => parseDataUrl,
26
27
  toAnthropic: () => toAnthropic,
28
+ toDataUrl: () => toDataUrl,
27
29
  toGemini: () => toGemini
28
30
  });
29
31
  module.exports = __toCommonJS(index_exports);
@@ -81,6 +83,68 @@ function unwrapResponse(response) {
81
83
  return JSON.stringify(response);
82
84
  }
83
85
 
86
+ // src/image.ts
87
+ var DATA_URL = /^data:([^;,]+);base64,(.*)$/s;
88
+ function parseDataUrl(url) {
89
+ const match = DATA_URL.exec(url);
90
+ if (!match) return null;
91
+ return { mediaType: match[1], data: match[2] };
92
+ }
93
+ function toDataUrl(mediaType, data) {
94
+ return `data:${mediaType};base64,${data}`;
95
+ }
96
+ function imageFromOpenAI(part) {
97
+ if (!isRecord(part) || part.type !== "image_url" || !isRecord(part.image_url)) return null;
98
+ const url = part.image_url.url;
99
+ if (typeof url !== "string") return null;
100
+ const parsed = parseDataUrl(url);
101
+ return parsed ? { kind: "base64", ...parsed } : { kind: "url", url };
102
+ }
103
+ function imageToOpenAI(image) {
104
+ const url = image.kind === "base64" ? toDataUrl(image.mediaType, image.data) : image.url;
105
+ return { type: "image_url", image_url: { url } };
106
+ }
107
+ function imageFromAnthropic(block) {
108
+ if (!isRecord(block) || block.type !== "image" || !isRecord(block.source)) return null;
109
+ const source = block.source;
110
+ if (source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string") {
111
+ return { kind: "base64", mediaType: source.media_type, data: source.data };
112
+ }
113
+ if (source.type === "url" && typeof source.url === "string") {
114
+ return { kind: "url", url: source.url };
115
+ }
116
+ return null;
117
+ }
118
+ function imageToAnthropic(image) {
119
+ if (image.kind === "base64") {
120
+ return { type: "image", source: { type: "base64", media_type: image.mediaType, data: image.data } };
121
+ }
122
+ return { type: "image", source: { type: "url", url: image.url } };
123
+ }
124
+ function imageFromGemini(part) {
125
+ if (isRecord(part) && isRecord(part.inlineData)) {
126
+ const data = part.inlineData;
127
+ if (typeof data.mimeType === "string" && typeof data.data === "string") {
128
+ return { kind: "base64", mediaType: data.mimeType, data: data.data };
129
+ }
130
+ }
131
+ if (isRecord(part) && isRecord(part.fileData)) {
132
+ const data = part.fileData;
133
+ if (typeof data.fileUri === "string") return { kind: "url", url: data.fileUri };
134
+ }
135
+ return null;
136
+ }
137
+ function imageToGemini(image, reporter) {
138
+ if (image.kind === "base64") {
139
+ return { inlineData: { mimeType: image.mediaType, data: image.data } };
140
+ }
141
+ reporter.warn(
142
+ "gemini-url-image",
143
+ "A remote image URL was emitted as Gemini fileData.fileUri; Gemini may require the Files API for non-Google URIs."
144
+ );
145
+ return { fileData: { fileUri: image.url } };
146
+ }
147
+
84
148
  // src/providers/openai.ts
85
149
  function isSystem(message) {
86
150
  return message.role === "system" || message.role === "developer";
@@ -142,9 +206,14 @@ function userContent(content, reporter) {
142
206
  for (const part of content) {
143
207
  if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
144
208
  blocks.push({ type: "text", text: part.text });
145
- } else {
146
- reporter.warn("dropped-content", "Dropped a non-text user content part not supported by this converter.");
209
+ continue;
210
+ }
211
+ const image = imageFromOpenAI(part);
212
+ if (image) {
213
+ blocks.push(imageToAnthropic(image));
214
+ continue;
147
215
  }
216
+ reporter.warn("dropped-content", "Dropped an unsupported user content part.");
148
217
  }
149
218
  return blocks;
150
219
  }
@@ -194,7 +263,7 @@ function fromAnthropic(conversation, options = {}) {
194
263
  const blocks = asBlocks(message.content);
195
264
  if (message.role === "user") {
196
265
  const toolResults = blocks.filter((b) => b.type === "tool_result");
197
- const textBlocks = blocks.filter((b) => b.type !== "tool_result");
266
+ const contentBlocks = blocks.filter((b) => b.type !== "tool_result");
198
267
  for (const block of toolResults) {
199
268
  out.push({
200
269
  role: "tool",
@@ -202,8 +271,8 @@ function fromAnthropic(conversation, options = {}) {
202
271
  content: textOf(block.content)
203
272
  });
204
273
  }
205
- if (textBlocks.length > 0) {
206
- out.push({ role: "user", content: textOf(textBlocks) });
274
+ if (contentBlocks.length > 0) {
275
+ out.push({ role: "user", content: userContentToOpenAI(contentBlocks) });
207
276
  }
208
277
  continue;
209
278
  }
@@ -220,6 +289,20 @@ function fromAnthropic(conversation, options = {}) {
220
289
  }
221
290
  return out;
222
291
  }
292
+ function userContentToOpenAI(blocks) {
293
+ const hasImage = blocks.some((block) => imageFromAnthropic(block) !== null);
294
+ if (!hasImage) return textOf(blocks);
295
+ const parts = [];
296
+ for (const block of blocks) {
297
+ const image = imageFromAnthropic(block);
298
+ if (image) {
299
+ parts.push(imageToOpenAI(image));
300
+ } else if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
301
+ parts.push({ type: "text", text: block.text });
302
+ }
303
+ }
304
+ return parts;
305
+ }
223
306
 
224
307
  // src/providers/gemini.ts
225
308
  function toGemini(messages, options = {}) {
@@ -274,9 +357,14 @@ function userParts(content, reporter) {
274
357
  for (const part of content) {
275
358
  if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
276
359
  parts.push({ text: part.text });
277
- } else {
278
- reporter.warn("dropped-content", "Dropped a non-text user content part not supported by this converter.");
360
+ continue;
361
+ }
362
+ const image = imageFromOpenAI(part);
363
+ if (image) {
364
+ parts.push(imageToGemini(image, reporter));
365
+ continue;
279
366
  }
367
+ reporter.warn("dropped-content", "Dropped an unsupported user content part.");
280
368
  }
281
369
  return parts.length > 0 ? parts : [{ text: "" }];
282
370
  }
@@ -321,7 +409,7 @@ function fromGemini(conversation, options = {}) {
321
409
  for (const content of conversation.contents) {
322
410
  const parts = Array.isArray(content.parts) ? content.parts : [];
323
411
  if (content.role === "model") {
324
- const textPieces2 = [];
412
+ const textPieces = [];
325
413
  const toolCalls = [];
326
414
  for (const part of parts) {
327
415
  if (isRecord(part) && isRecord(part.functionCall)) {
@@ -335,27 +423,42 @@ function fromGemini(conversation, options = {}) {
335
423
  });
336
424
  pending.push({ id, name: fc.name });
337
425
  } else if (isRecord(part) && typeof part.text === "string") {
338
- textPieces2.push(part.text);
426
+ textPieces.push(part.text);
339
427
  }
340
428
  }
341
- const text2 = textPieces2.join("");
342
- const assistant = { role: "assistant", content: text2 || null };
429
+ const text = textPieces.join("");
430
+ const assistant = { role: "assistant", content: text || null };
343
431
  if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
344
432
  out.push(assistant);
345
433
  continue;
346
434
  }
347
- const textPieces = [];
435
+ const contentParts = [];
436
+ let hasImage = false;
348
437
  for (const part of parts) {
349
438
  if (isRecord(part) && isRecord(part.functionResponse)) {
350
439
  const fr = part.functionResponse;
351
440
  const id = resolveResponseId(fr, pending, reporter, generateId);
352
441
  out.push({ role: "tool", tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });
353
- } else if (isRecord(part) && typeof part.text === "string") {
354
- textPieces.push(part.text);
442
+ continue;
443
+ }
444
+ const image = imageFromGemini(part);
445
+ if (image) {
446
+ contentParts.push(imageToOpenAI(image));
447
+ hasImage = true;
448
+ continue;
449
+ }
450
+ if (isRecord(part) && typeof part.text === "string") {
451
+ contentParts.push({ type: "text", text: part.text });
452
+ }
453
+ }
454
+ if (contentParts.length > 0) {
455
+ if (hasImage) {
456
+ out.push({ role: "user", content: contentParts });
457
+ } else {
458
+ const text = textOf(contentParts);
459
+ if (text) out.push({ role: "user", content: text });
355
460
  }
356
461
  }
357
- const text = textPieces.join("");
358
- if (text) out.push({ role: "user", content: text });
359
462
  }
360
463
  return out;
361
464
  }
@@ -413,7 +516,9 @@ function fromCanonical(canonical, to, options) {
413
516
  convert,
414
517
  fromAnthropic,
415
518
  fromGemini,
519
+ parseDataUrl,
416
520
  toAnthropic,
521
+ toDataUrl,
417
522
  toGemini
418
523
  });
419
524
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/util.ts","../src/providers/openai.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/convert.ts"],"sourcesContent":["export { toAnthropic, fromAnthropic } from './providers/anthropic.js';\nexport { toGemini, fromGemini } from './providers/gemini.js';\nexport { convert } from './convert.js';\nexport type { ConversationOf } from './convert.js';\n\nexport type {\n Provider,\n Warning,\n WarningCode,\n ConvertOptions,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIUserMessage,\n OpenAIAssistantMessage,\n OpenAIToolMessage,\n OpenAIToolCall,\n OpenAIContentPart,\n OpenAITextPart,\n AnthropicConversation,\n AnthropicMessage,\n AnthropicContentBlock,\n AnthropicTextBlock,\n AnthropicToolUseBlock,\n AnthropicToolResultBlock,\n GeminiConversation,\n GeminiContent,\n GeminiPart,\n GeminiTextPart,\n GeminiFunctionCallPart,\n GeminiFunctionResponsePart,\n} from './types.js';\n","import type { ConvertOptions, OpenAITextPart, Warning, WarningCode } from './types.js';\n\n/** Thin wrapper around the optional `onWarning` callback. */\nexport class Reporter {\n constructor(private readonly options: ConvertOptions = {}) {}\n\n warn(code: WarningCode, message: string): void {\n this.options.onWarning?.({ code, message } satisfies Warning);\n }\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Flattens any supported text content (a plain string or an array of text\n * parts/blocks) into a single string. Non text parts are ignored.\n */\nexport function textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (isRecord(part) && typeof part.text === 'string') return part.text;\n return '';\n })\n .join('');\n }\n return '';\n}\n\n/** Wraps a string as a single OpenAI/Anthropic text part array. */\nexport function textPart(text: string): OpenAITextPart[] {\n return [{ type: 'text', text }];\n}\n\n/** Safely parses a JSON string, returning a discriminated result. */\nexport function tryParseJson(input: string): { ok: true; value: unknown } | { ok: false } {\n try {\n return { ok: true, value: JSON.parse(input) };\n } catch {\n return { ok: false };\n }\n}\n\n/**\n * Parses an OpenAI tool-call `arguments` JSON string into an object. Reports and\n * returns `{}` when the string is not valid JSON object syntax.\n */\nexport function parseArguments(args: string, reporter: Reporter, fnName: string): Record<string, unknown> {\n const parsed = tryParseJson(args);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n reporter.warn(\n 'invalid-json-arguments',\n `Tool call '${fnName}' had arguments that were not a JSON object; used an empty object instead.`,\n );\n return {};\n}\n\n/**\n * Converts an OpenAI tool result string into a Gemini `functionResponse.response`\n * object. A JSON object string is used directly; anything else is wrapped as\n * `{ result: <text> }`, which {@link unwrapResponse} reverses.\n */\nexport function wrapResponse(content: string): Record<string, unknown> {\n const parsed = tryParseJson(content);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n return { result: content };\n}\n\n/** Reverses {@link wrapResponse}: a lone `{ result: string }` becomes that string. */\nexport function unwrapResponse(response: Record<string, unknown>): string {\n const keys = Object.keys(response);\n if (keys.length === 1 && keys[0] === 'result' && typeof response.result === 'string') {\n return response.result;\n }\n return JSON.stringify(response);\n}\n","import type {\n OpenAIAssistantMessage,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIToolMessage,\n OpenAIUserMessage,\n} from '../types.js';\nimport { Reporter, textOf } from '../util.js';\n\n/** Any canonical message other than a system/developer prompt. */\nexport type NonSystemMessage = OpenAIUserMessage | OpenAIAssistantMessage | OpenAIToolMessage;\n\nfunction isSystem(message: OpenAIMessage): message is OpenAISystemMessage {\n return message.role === 'system' || message.role === 'developer';\n}\n\n/**\n * Splits a canonical OpenAI conversation into its system prompt and the\n * remaining messages. All `system` / `developer` messages are folded into a\n * single string (joined by blank lines) because Anthropic and Gemini carry the\n * system prompt as one top-level field. A system message that appears after the\n * conversation has started is still folded in, but reported as `system-midstream`.\n */\nexport function splitSystem(\n messages: OpenAIMessage[],\n reporter: Reporter,\n): { system?: string; rest: NonSystemMessage[] } {\n const systemParts: string[] = [];\n const rest: NonSystemMessage[] = [];\n let started = false;\n\n for (const message of messages) {\n if (isSystem(message)) {\n if (started) {\n reporter.warn(\n 'system-midstream',\n 'A system message appeared mid conversation; it was merged into the top-level system prompt.',\n );\n }\n systemParts.push(textOf(message.content));\n continue;\n }\n started = true;\n rest.push(message);\n }\n\n const system = systemParts.length > 0 ? systemParts.join('\\n\\n') : undefined;\n return system === undefined ? { rest } : { system, rest };\n}\n","import type {\n AnthropicContentBlock,\n AnthropicConversation,\n AnthropicMessage,\n ConvertOptions,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf } from '../util.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Anthropic */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into an Anthropic request fragment\n * (`{ system, messages }`). System messages move to the top-level `system`\n * field, tool-call arguments are JSON parsed into `input` objects, tool results\n * are folded into `tool_result` blocks inside a user turn, and consecutive\n * same-role turns are merged to satisfy Anthropic's strict alternation.\n */\nexport function toAnthropic(messages: OpenAIMessage[], options: ConvertOptions = {}): AnthropicConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n const out: AnthropicMessage[] = [];\n\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const blocks: AnthropicContentBlock[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n blocks.push({ type: 'tool_result', tool_use_id: tool.tool_call_id, content: textOf(tool.content) });\n j++;\n }\n out.push({ role: 'user', content: blocks });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n out.push({ role: 'user', content: userContent(message.content, reporter) });\n continue;\n }\n\n out.push({ role: 'assistant', content: assistantContent(message, reporter) });\n }\n\n const merged = mergeConsecutive(out, reporter);\n return system === undefined ? { messages: merged } : { system, messages: merged };\n}\n\nfunction userContent(content: string | OpenAIContentPart[], reporter: Reporter): string | AnthropicContentBlock[] {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return textOf(content);\n const blocks: AnthropicContentBlock[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n blocks.push({ type: 'text', text: part.text });\n } else {\n reporter.warn('dropped-content', 'Dropped a non-text user content part not supported by this converter.');\n }\n }\n return blocks;\n}\n\nfunction assistantContent(message: OpenAIAssistantMessage, reporter: Reporter): string | AnthropicContentBlock[] {\n const text = textOf(message.content ?? '');\n const toolCalls = message.tool_calls ?? [];\n if (toolCalls.length === 0) return text;\n\n const blocks: AnthropicContentBlock[] = [];\n if (text) blocks.push({ type: 'text', text });\n for (const call of toolCalls) {\n blocks.push({\n type: 'tool_use',\n id: call.id,\n name: call.function.name,\n input: parseArguments(call.function.arguments, reporter, call.function.name),\n });\n }\n return blocks;\n}\n\n/** Merges adjacent same-role messages by concatenating their content blocks. */\nfunction mergeConsecutive(messages: AnthropicMessage[], reporter: Reporter): AnthropicMessage[] {\n const result: AnthropicMessage[] = [];\n for (const message of messages) {\n const previous = result[result.length - 1];\n if (previous && previous.role === message.role) {\n previous.content = [...asBlocks(previous.content), ...asBlocks(message.content)];\n reporter.warn(\n 'merged-role',\n `Merged consecutive '${message.role}' turns (Anthropic requires alternating roles).`,\n );\n } else {\n result.push({ role: message.role, content: message.content });\n }\n }\n return result;\n}\n\nfunction asBlocks(content: string | AnthropicContentBlock[]): AnthropicContentBlock[] {\n if (typeof content !== 'string') return content;\n return content ? [{ type: 'text', text: content }] : [];\n}\n\n/* ------------------------------------------------------------------ */\n/* Anthropic -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts an Anthropic request fragment back into a canonical OpenAI message\n * array. The top-level `system` becomes a leading system message, `tool_use`\n * blocks become `tool_calls` (with `input` re-serialized to a JSON string), and\n * `tool_result` blocks become standalone `role: 'tool'` messages.\n */\nexport function fromAnthropic(conversation: AnthropicConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n void options;\n const out: OpenAIMessage[] = [];\n if (conversation.system) {\n out.push({ role: 'system', content: textOf(conversation.system) });\n }\n\n for (const message of conversation.messages) {\n const blocks = asBlocks(message.content);\n\n if (message.role === 'user') {\n const toolResults = blocks.filter((b) => b.type === 'tool_result');\n const textBlocks = blocks.filter((b) => b.type !== 'tool_result');\n for (const block of toolResults) {\n out.push({\n role: 'tool',\n tool_call_id: String((block as { tool_use_id?: string }).tool_use_id ?? ''),\n content: textOf((block as { content?: unknown }).content),\n });\n }\n if (textBlocks.length > 0) {\n out.push({ role: 'user', content: textOf(textBlocks) });\n }\n continue;\n }\n\n // assistant\n const text = textOf(blocks.filter((b) => b.type === 'text'));\n const toolUses = blocks.filter((b) => b.type === 'tool_use');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolUses.length > 0) {\n assistant.tool_calls = toolUses.map((block) => {\n const b = block as { id: string; name: string; input: Record<string, unknown> };\n return { id: b.id, type: 'function', function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };\n });\n }\n out.push(assistant);\n }\n\n return out;\n}\n","import type {\n ConvertOptions,\n GeminiContent,\n GeminiConversation,\n GeminiPart,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolCall,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf, unwrapResponse, wrapResponse } from '../util.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Gemini */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into a Gemini request fragment\n * (`{ systemInstruction, contents }`). The assistant role becomes `model`,\n * tool-call arguments are JSON parsed into `args` objects, tool results become\n * `functionResponse` parts whose `name` is recovered from the matching call, and\n * consecutive same-role turns are merged for Gemini's strict alternation.\n *\n * The OpenAI tool-call `id` is carried through as `functionCall.id` so that a\n * round trip (OpenAI -> Gemini -> OpenAI) preserves ids exactly.\n */\nexport function toGemini(messages: OpenAIMessage[], options: ConvertOptions = {}): GeminiConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n\n const idToName = new Map<string, string>();\n for (const message of rest) {\n if (message.role === 'assistant' && message.tool_calls) {\n for (const call of message.tool_calls) idToName.set(call.id, call.function.name);\n }\n }\n\n const contents: GeminiContent[] = [];\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const parts: GeminiPart[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n const name = idToName.get(tool.tool_call_id);\n if (!name) {\n reporter.warn(\n 'unmapped-tool-result',\n `Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`,\n );\n }\n parts.push({\n functionResponse: {\n id: tool.tool_call_id,\n name: name ?? tool.tool_call_id,\n response: wrapResponse(textOf(tool.content)),\n },\n });\n j++;\n }\n contents.push({ role: 'user', parts });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n contents.push({ role: 'user', parts: userParts(message.content, reporter) });\n continue;\n }\n\n contents.push({ role: 'model', parts: assistantParts(message, reporter) });\n }\n\n const merged = mergeConsecutive(contents, reporter);\n return system === undefined\n ? { contents: merged }\n : { systemInstruction: { parts: [{ text: system }] }, contents: merged };\n}\n\nfunction userParts(content: string | OpenAIContentPart[], reporter: Reporter): GeminiPart[] {\n if (typeof content === 'string') return [{ text: content }];\n const parts: GeminiPart[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n parts.push({ text: part.text });\n } else {\n reporter.warn('dropped-content', 'Dropped a non-text user content part not supported by this converter.');\n }\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\nfunction assistantParts(message: OpenAIAssistantMessage, reporter: Reporter): GeminiPart[] {\n const parts: GeminiPart[] = [];\n const text = textOf(message.content ?? '');\n if (text) parts.push({ text });\n for (const call of message.tool_calls ?? []) {\n parts.push({\n functionCall: {\n id: call.id,\n name: call.function.name,\n args: parseArguments(call.function.arguments, reporter, call.function.name),\n },\n });\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\n/** Merges adjacent same-role contents by concatenating their `parts` arrays. */\nfunction mergeConsecutive(contents: GeminiContent[], reporter: Reporter): GeminiContent[] {\n const result: GeminiContent[] = [];\n for (const content of contents) {\n const previous = result[result.length - 1];\n if (previous && previous.role === content.role) {\n previous.parts = [...previous.parts, ...content.parts];\n reporter.warn('merged-role', `Merged consecutive '${content.role}' turns (Gemini requires alternating roles).`);\n } else {\n result.push({ role: content.role, parts: [...content.parts] });\n }\n }\n return result;\n}\n\n/* ------------------------------------------------------------------ */\n/* Gemini -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a Gemini request fragment back into a canonical OpenAI message array.\n * Because Gemini matches tool calls and responses by function name (the `id`\n * field is optional), this maintains a queue of pending calls and resolves each\n * `functionResponse` by id when present, otherwise by name in call order,\n * generating a deterministic id as a last resort.\n */\nexport function fromGemini(conversation: GeminiConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n const reporter = new Reporter(options);\n const out: OpenAIMessage[] = [];\n\n if (conversation.systemInstruction) {\n const text = textOf(conversation.systemInstruction.parts);\n if (text) out.push({ role: 'system', content: text });\n }\n\n const pending: { id: string; name: string }[] = [];\n let counter = 0;\n const generateId = (name: string): string => `call_${name.replace(/[^a-zA-Z0-9_-]/g, '_')}_${counter++}`;\n\n for (const content of conversation.contents) {\n const parts = Array.isArray(content.parts) ? content.parts : [];\n\n if (content.role === 'model') {\n const textPieces: string[] = [];\n const toolCalls: OpenAIToolCall[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionCall)) {\n const fc = part.functionCall as { id?: string; name: string; args?: Record<string, unknown> };\n const id = fc.id ?? generateId(fc.name);\n if (!fc.id) reporter.warn('generated-id', `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);\n toolCalls.push({\n id,\n type: 'function',\n function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) },\n });\n pending.push({ id, name: fc.name });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolCalls.length > 0) assistant.tool_calls = toolCalls;\n out.push(assistant);\n continue;\n }\n\n // role 'user' or unspecified\n const textPieces: string[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionResponse)) {\n const fr = part.functionResponse as { id?: string; name: string; response?: Record<string, unknown> };\n const id = resolveResponseId(fr, pending, reporter, generateId);\n out.push({ role: 'tool', tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n if (text) out.push({ role: 'user', content: text });\n }\n\n return out;\n}\n\nfunction resolveResponseId(\n response: { id?: string; name: string },\n pending: { id: string; name: string }[],\n reporter: Reporter,\n generateId: (name: string) => string,\n): string {\n if (response.id) {\n const index = pending.findIndex((p) => p.id === response.id);\n if (index >= 0) pending.splice(index, 1);\n return response.id;\n }\n const index = pending.findIndex((p) => p.name === response.name);\n if (index >= 0) {\n const { id } = pending[index];\n pending.splice(index, 1);\n return id;\n }\n const id = generateId(response.name);\n reporter.warn(\n 'unmapped-tool-result',\n `Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`,\n );\n return id;\n}\n","import type { AnthropicConversation, ConvertOptions, GeminiConversation, OpenAIMessage, Provider } from './types.js';\nimport { fromAnthropic, toAnthropic } from './providers/anthropic.js';\nimport { fromGemini, toGemini } from './providers/gemini.js';\n\n/** Maps a provider to the conversation shape it accepts and returns. */\nexport type ConversationOf<P extends Provider> = P extends 'openai'\n ? OpenAIMessage[]\n : P extends 'anthropic'\n ? AnthropicConversation\n : P extends 'gemini'\n ? GeminiConversation\n : never;\n\n/**\n * Converts a conversation from one provider format to another. Every conversion\n * routes through the canonical OpenAI representation, so any source/target pair\n * is supported, including same-provider normalization.\n *\n * @example\n * const gemini = convert(openaiMessages, { from: 'openai', to: 'gemini' });\n * const openai = convert(anthropicBody, { from: 'anthropic', to: 'openai' });\n */\nexport function convert<From extends Provider, To extends Provider>(\n conversation: ConversationOf<From>,\n route: { from: From; to: To },\n options: ConvertOptions = {},\n): ConversationOf<To> {\n const canonical = toCanonical(conversation, route.from, options);\n return fromCanonical(canonical, route.to, options) as ConversationOf<To>;\n}\n\nfunction toCanonical(conversation: unknown, from: Provider, options: ConvertOptions): OpenAIMessage[] {\n switch (from) {\n case 'openai':\n return conversation as OpenAIMessage[];\n case 'anthropic':\n return fromAnthropic(conversation as AnthropicConversation, options);\n case 'gemini':\n return fromGemini(conversation as GeminiConversation, options);\n default:\n throw new Error(`Unknown source provider: ${String(from)}`);\n }\n}\n\nfunction fromCanonical(canonical: OpenAIMessage[], to: Provider, options: ConvertOptions): ConversationOf<Provider> {\n switch (to) {\n case 'openai':\n return canonical;\n case 'anthropic':\n return toAnthropic(canonical, options);\n case 'gemini':\n return toGemini(canonical, options);\n default:\n throw new Error(`Unknown target provider: ${String(to)}`);\n }\n}\n"],"mappings":"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","names":["mergeConsecutive","textPieces","text","index","id"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/util.ts","../src/image.ts","../src/providers/openai.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/convert.ts"],"sourcesContent":["export { toAnthropic, fromAnthropic } from './providers/anthropic.js';\nexport { toGemini, fromGemini } from './providers/gemini.js';\nexport { convert } from './convert.js';\nexport type { ConversationOf } from './convert.js';\nexport { parseDataUrl, toDataUrl } from './image.js';\nexport type { NormalizedImage } from './image.js';\n\nexport type {\n Provider,\n Warning,\n WarningCode,\n ConvertOptions,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIUserMessage,\n OpenAIAssistantMessage,\n OpenAIToolMessage,\n OpenAIToolCall,\n OpenAIContentPart,\n OpenAITextPart,\n OpenAIImagePart,\n AnthropicConversation,\n AnthropicMessage,\n AnthropicContentBlock,\n AnthropicTextBlock,\n AnthropicToolUseBlock,\n AnthropicToolResultBlock,\n AnthropicImageBlock,\n GeminiConversation,\n GeminiContent,\n GeminiPart,\n GeminiTextPart,\n GeminiFunctionCallPart,\n GeminiFunctionResponsePart,\n GeminiInlineDataPart,\n GeminiFileDataPart,\n} from './types.js';\n","import type { ConvertOptions, OpenAITextPart, Warning, WarningCode } from './types.js';\n\n/** Thin wrapper around the optional `onWarning` callback. */\nexport class Reporter {\n constructor(private readonly options: ConvertOptions = {}) {}\n\n warn(code: WarningCode, message: string): void {\n this.options.onWarning?.({ code, message } satisfies Warning);\n }\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Flattens any supported text content (a plain string or an array of text\n * parts/blocks) into a single string. Non text parts are ignored.\n */\nexport function textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (isRecord(part) && typeof part.text === 'string') return part.text;\n return '';\n })\n .join('');\n }\n return '';\n}\n\n/** Wraps a string as a single OpenAI/Anthropic text part array. */\nexport function textPart(text: string): OpenAITextPart[] {\n return [{ type: 'text', text }];\n}\n\n/** Safely parses a JSON string, returning a discriminated result. */\nexport function tryParseJson(input: string): { ok: true; value: unknown } | { ok: false } {\n try {\n return { ok: true, value: JSON.parse(input) };\n } catch {\n return { ok: false };\n }\n}\n\n/**\n * Parses an OpenAI tool-call `arguments` JSON string into an object. Reports and\n * returns `{}` when the string is not valid JSON object syntax.\n */\nexport function parseArguments(args: string, reporter: Reporter, fnName: string): Record<string, unknown> {\n const parsed = tryParseJson(args);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n reporter.warn(\n 'invalid-json-arguments',\n `Tool call '${fnName}' had arguments that were not a JSON object; used an empty object instead.`,\n );\n return {};\n}\n\n/**\n * Converts an OpenAI tool result string into a Gemini `functionResponse.response`\n * object. A JSON object string is used directly; anything else is wrapped as\n * `{ result: <text> }`, which {@link unwrapResponse} reverses.\n */\nexport function wrapResponse(content: string): Record<string, unknown> {\n const parsed = tryParseJson(content);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n return { result: content };\n}\n\n/** Reverses {@link wrapResponse}: a lone `{ result: string }` becomes that string. */\nexport function unwrapResponse(response: Record<string, unknown>): string {\n const keys = Object.keys(response);\n if (keys.length === 1 && keys[0] === 'result' && typeof response.result === 'string') {\n return response.result;\n }\n return JSON.stringify(response);\n}\n","import type { AnthropicImageBlock, GeminiPart, OpenAIImagePart } from './types.js';\nimport { Reporter, isRecord } from './util.js';\n\n/**\n * A provider-neutral image. `base64` carries the raw bytes plus media type\n * (mapping to a data URL, an Anthropic base64 source or a Gemini `inlineData`\n * part); `url` carries a remote reference.\n */\nexport type NormalizedImage = { kind: 'base64'; mediaType: string; data: string } | { kind: 'url'; url: string };\n\nconst DATA_URL = /^data:([^;,]+);base64,(.*)$/s;\n\n/** Decomposes a `data:<mediaType>;base64,<data>` URL. Returns null otherwise. */\nexport function parseDataUrl(url: string): { mediaType: string; data: string } | null {\n const match = DATA_URL.exec(url);\n if (!match) return null;\n return { mediaType: match[1], data: match[2] };\n}\n\n/** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */\nexport function toDataUrl(mediaType: string, data: string): string {\n return `data:${mediaType};base64,${data}`;\n}\n\n/* ------------------------------- OpenAI -------------------------------- */\n\nexport function imageFromOpenAI(part: unknown): NormalizedImage | null {\n if (!isRecord(part) || part.type !== 'image_url' || !isRecord(part.image_url)) return null;\n const url = part.image_url.url;\n if (typeof url !== 'string') return null;\n const parsed = parseDataUrl(url);\n return parsed ? { kind: 'base64', ...parsed } : { kind: 'url', url };\n}\n\nexport function imageToOpenAI(image: NormalizedImage): OpenAIImagePart {\n const url = image.kind === 'base64' ? toDataUrl(image.mediaType, image.data) : image.url;\n return { type: 'image_url', image_url: { url } };\n}\n\n/* ------------------------------ Anthropic ------------------------------ */\n\nexport function imageFromAnthropic(block: unknown): NormalizedImage | null {\n if (!isRecord(block) || block.type !== 'image' || !isRecord(block.source)) return null;\n const source = block.source;\n if (source.type === 'base64' && typeof source.media_type === 'string' && typeof source.data === 'string') {\n return { kind: 'base64', mediaType: source.media_type, data: source.data };\n }\n if (source.type === 'url' && typeof source.url === 'string') {\n return { kind: 'url', url: source.url };\n }\n return null;\n}\n\nexport function imageToAnthropic(image: NormalizedImage): AnthropicImageBlock {\n if (image.kind === 'base64') {\n return { type: 'image', source: { type: 'base64', media_type: image.mediaType, data: image.data } };\n }\n return { type: 'image', source: { type: 'url', url: image.url } };\n}\n\n/* -------------------------------- Gemini ------------------------------- */\n\nexport function imageFromGemini(part: unknown): NormalizedImage | null {\n if (isRecord(part) && isRecord(part.inlineData)) {\n const data = part.inlineData;\n if (typeof data.mimeType === 'string' && typeof data.data === 'string') {\n return { kind: 'base64', mediaType: data.mimeType, data: data.data };\n }\n }\n if (isRecord(part) && isRecord(part.fileData)) {\n const data = part.fileData;\n if (typeof data.fileUri === 'string') return { kind: 'url', url: data.fileUri };\n }\n return null;\n}\n\nexport function imageToGemini(image: NormalizedImage, reporter: Reporter): GeminiPart {\n if (image.kind === 'base64') {\n return { inlineData: { mimeType: image.mediaType, data: image.data } };\n }\n // Gemini's fileData.fileUri is intended for Files API URIs, not arbitrary\n // public URLs. We emit it as a best effort and flag it so the caller can\n // upload via the Files API or inline the bytes if Gemini rejects it.\n reporter.warn(\n 'gemini-url-image',\n 'A remote image URL was emitted as Gemini fileData.fileUri; Gemini may require the Files API for non-Google URIs.',\n );\n return { fileData: { fileUri: image.url } };\n}\n","import type {\n OpenAIAssistantMessage,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIToolMessage,\n OpenAIUserMessage,\n} from '../types.js';\nimport { Reporter, textOf } from '../util.js';\n\n/** Any canonical message other than a system/developer prompt. */\nexport type NonSystemMessage = OpenAIUserMessage | OpenAIAssistantMessage | OpenAIToolMessage;\n\nfunction isSystem(message: OpenAIMessage): message is OpenAISystemMessage {\n return message.role === 'system' || message.role === 'developer';\n}\n\n/**\n * Splits a canonical OpenAI conversation into its system prompt and the\n * remaining messages. All `system` / `developer` messages are folded into a\n * single string (joined by blank lines) because Anthropic and Gemini carry the\n * system prompt as one top-level field. A system message that appears after the\n * conversation has started is still folded in, but reported as `system-midstream`.\n */\nexport function splitSystem(\n messages: OpenAIMessage[],\n reporter: Reporter,\n): { system?: string; rest: NonSystemMessage[] } {\n const systemParts: string[] = [];\n const rest: NonSystemMessage[] = [];\n let started = false;\n\n for (const message of messages) {\n if (isSystem(message)) {\n if (started) {\n reporter.warn(\n 'system-midstream',\n 'A system message appeared mid conversation; it was merged into the top-level system prompt.',\n );\n }\n systemParts.push(textOf(message.content));\n continue;\n }\n started = true;\n rest.push(message);\n }\n\n const system = systemParts.length > 0 ? systemParts.join('\\n\\n') : undefined;\n return system === undefined ? { rest } : { system, rest };\n}\n","import type {\n AnthropicContentBlock,\n AnthropicConversation,\n AnthropicMessage,\n ConvertOptions,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf } from '../util.js';\nimport { imageFromAnthropic, imageFromOpenAI, imageToAnthropic, imageToOpenAI } from '../image.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Anthropic */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into an Anthropic request fragment\n * (`{ system, messages }`). System messages move to the top-level `system`\n * field, tool-call arguments are JSON parsed into `input` objects, tool results\n * are folded into `tool_result` blocks inside a user turn, and consecutive\n * same-role turns are merged to satisfy Anthropic's strict alternation.\n */\nexport function toAnthropic(messages: OpenAIMessage[], options: ConvertOptions = {}): AnthropicConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n const out: AnthropicMessage[] = [];\n\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const blocks: AnthropicContentBlock[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n blocks.push({ type: 'tool_result', tool_use_id: tool.tool_call_id, content: textOf(tool.content) });\n j++;\n }\n out.push({ role: 'user', content: blocks });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n out.push({ role: 'user', content: userContent(message.content, reporter) });\n continue;\n }\n\n out.push({ role: 'assistant', content: assistantContent(message, reporter) });\n }\n\n const merged = mergeConsecutive(out, reporter);\n return system === undefined ? { messages: merged } : { system, messages: merged };\n}\n\nfunction userContent(content: string | OpenAIContentPart[], reporter: Reporter): string | AnthropicContentBlock[] {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return textOf(content);\n const blocks: AnthropicContentBlock[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n blocks.push({ type: 'text', text: part.text });\n continue;\n }\n const image = imageFromOpenAI(part);\n if (image) {\n blocks.push(imageToAnthropic(image));\n continue;\n }\n reporter.warn('dropped-content', 'Dropped an unsupported user content part.');\n }\n return blocks;\n}\n\nfunction assistantContent(message: OpenAIAssistantMessage, reporter: Reporter): string | AnthropicContentBlock[] {\n const text = textOf(message.content ?? '');\n const toolCalls = message.tool_calls ?? [];\n if (toolCalls.length === 0) return text;\n\n const blocks: AnthropicContentBlock[] = [];\n if (text) blocks.push({ type: 'text', text });\n for (const call of toolCalls) {\n blocks.push({\n type: 'tool_use',\n id: call.id,\n name: call.function.name,\n input: parseArguments(call.function.arguments, reporter, call.function.name),\n });\n }\n return blocks;\n}\n\n/** Merges adjacent same-role messages by concatenating their content blocks. */\nfunction mergeConsecutive(messages: AnthropicMessage[], reporter: Reporter): AnthropicMessage[] {\n const result: AnthropicMessage[] = [];\n for (const message of messages) {\n const previous = result[result.length - 1];\n if (previous && previous.role === message.role) {\n previous.content = [...asBlocks(previous.content), ...asBlocks(message.content)];\n reporter.warn(\n 'merged-role',\n `Merged consecutive '${message.role}' turns (Anthropic requires alternating roles).`,\n );\n } else {\n result.push({ role: message.role, content: message.content });\n }\n }\n return result;\n}\n\nfunction asBlocks(content: string | AnthropicContentBlock[]): AnthropicContentBlock[] {\n if (typeof content !== 'string') return content;\n return content ? [{ type: 'text', text: content }] : [];\n}\n\n/* ------------------------------------------------------------------ */\n/* Anthropic -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts an Anthropic request fragment back into a canonical OpenAI message\n * array. The top-level `system` becomes a leading system message, `tool_use`\n * blocks become `tool_calls` (with `input` re-serialized to a JSON string), and\n * `tool_result` blocks become standalone `role: 'tool'` messages.\n */\nexport function fromAnthropic(conversation: AnthropicConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n void options;\n const out: OpenAIMessage[] = [];\n if (conversation.system) {\n out.push({ role: 'system', content: textOf(conversation.system) });\n }\n\n for (const message of conversation.messages) {\n const blocks = asBlocks(message.content);\n\n if (message.role === 'user') {\n const toolResults = blocks.filter((b) => b.type === 'tool_result');\n const contentBlocks = blocks.filter((b) => b.type !== 'tool_result');\n for (const block of toolResults) {\n out.push({\n role: 'tool',\n tool_call_id: String((block as { tool_use_id?: string }).tool_use_id ?? ''),\n content: textOf((block as { content?: unknown }).content),\n });\n }\n if (contentBlocks.length > 0) {\n out.push({ role: 'user', content: userContentToOpenAI(contentBlocks) });\n }\n continue;\n }\n\n // assistant\n const text = textOf(blocks.filter((b) => b.type === 'text'));\n const toolUses = blocks.filter((b) => b.type === 'tool_use');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolUses.length > 0) {\n assistant.tool_calls = toolUses.map((block) => {\n const b = block as { id: string; name: string; input: Record<string, unknown> };\n return { id: b.id, type: 'function', function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };\n });\n }\n out.push(assistant);\n }\n\n return out;\n}\n\n/** Rebuilds OpenAI user content from Anthropic blocks, as a string unless an image is present. */\nfunction userContentToOpenAI(blocks: AnthropicContentBlock[]): string | OpenAIContentPart[] {\n const hasImage = blocks.some((block) => imageFromAnthropic(block) !== null);\n if (!hasImage) return textOf(blocks);\n const parts: OpenAIContentPart[] = [];\n for (const block of blocks) {\n const image = imageFromAnthropic(block);\n if (image) {\n parts.push(imageToOpenAI(image));\n } else if (isRecord(block) && block.type === 'text' && typeof block.text === 'string') {\n parts.push({ type: 'text', text: block.text });\n }\n }\n return parts;\n}\n","import type {\n ConvertOptions,\n GeminiContent,\n GeminiConversation,\n GeminiPart,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolCall,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf, unwrapResponse, wrapResponse } from '../util.js';\nimport { imageFromGemini, imageFromOpenAI, imageToGemini, imageToOpenAI } from '../image.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Gemini */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into a Gemini request fragment\n * (`{ systemInstruction, contents }`). The assistant role becomes `model`,\n * tool-call arguments are JSON parsed into `args` objects, tool results become\n * `functionResponse` parts whose `name` is recovered from the matching call, and\n * consecutive same-role turns are merged for Gemini's strict alternation.\n *\n * The OpenAI tool-call `id` is carried through as `functionCall.id` so that a\n * round trip (OpenAI -> Gemini -> OpenAI) preserves ids exactly.\n */\nexport function toGemini(messages: OpenAIMessage[], options: ConvertOptions = {}): GeminiConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n\n const idToName = new Map<string, string>();\n for (const message of rest) {\n if (message.role === 'assistant' && message.tool_calls) {\n for (const call of message.tool_calls) idToName.set(call.id, call.function.name);\n }\n }\n\n const contents: GeminiContent[] = [];\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const parts: GeminiPart[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n const name = idToName.get(tool.tool_call_id);\n if (!name) {\n reporter.warn(\n 'unmapped-tool-result',\n `Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`,\n );\n }\n parts.push({\n functionResponse: {\n id: tool.tool_call_id,\n name: name ?? tool.tool_call_id,\n response: wrapResponse(textOf(tool.content)),\n },\n });\n j++;\n }\n contents.push({ role: 'user', parts });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n contents.push({ role: 'user', parts: userParts(message.content, reporter) });\n continue;\n }\n\n contents.push({ role: 'model', parts: assistantParts(message, reporter) });\n }\n\n const merged = mergeConsecutive(contents, reporter);\n return system === undefined\n ? { contents: merged }\n : { systemInstruction: { parts: [{ text: system }] }, contents: merged };\n}\n\nfunction userParts(content: string | OpenAIContentPart[], reporter: Reporter): GeminiPart[] {\n if (typeof content === 'string') return [{ text: content }];\n const parts: GeminiPart[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n parts.push({ text: part.text });\n continue;\n }\n const image = imageFromOpenAI(part);\n if (image) {\n parts.push(imageToGemini(image, reporter));\n continue;\n }\n reporter.warn('dropped-content', 'Dropped an unsupported user content part.');\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\nfunction assistantParts(message: OpenAIAssistantMessage, reporter: Reporter): GeminiPart[] {\n const parts: GeminiPart[] = [];\n const text = textOf(message.content ?? '');\n if (text) parts.push({ text });\n for (const call of message.tool_calls ?? []) {\n parts.push({\n functionCall: {\n id: call.id,\n name: call.function.name,\n args: parseArguments(call.function.arguments, reporter, call.function.name),\n },\n });\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\n/** Merges adjacent same-role contents by concatenating their `parts` arrays. */\nfunction mergeConsecutive(contents: GeminiContent[], reporter: Reporter): GeminiContent[] {\n const result: GeminiContent[] = [];\n for (const content of contents) {\n const previous = result[result.length - 1];\n if (previous && previous.role === content.role) {\n previous.parts = [...previous.parts, ...content.parts];\n reporter.warn('merged-role', `Merged consecutive '${content.role}' turns (Gemini requires alternating roles).`);\n } else {\n result.push({ role: content.role, parts: [...content.parts] });\n }\n }\n return result;\n}\n\n/* ------------------------------------------------------------------ */\n/* Gemini -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a Gemini request fragment back into a canonical OpenAI message array.\n * Because Gemini matches tool calls and responses by function name (the `id`\n * field is optional), this maintains a queue of pending calls and resolves each\n * `functionResponse` by id when present, otherwise by name in call order,\n * generating a deterministic id as a last resort.\n */\nexport function fromGemini(conversation: GeminiConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n const reporter = new Reporter(options);\n const out: OpenAIMessage[] = [];\n\n if (conversation.systemInstruction) {\n const text = textOf(conversation.systemInstruction.parts);\n if (text) out.push({ role: 'system', content: text });\n }\n\n const pending: { id: string; name: string }[] = [];\n let counter = 0;\n const generateId = (name: string): string => `call_${name.replace(/[^a-zA-Z0-9_-]/g, '_')}_${counter++}`;\n\n for (const content of conversation.contents) {\n const parts = Array.isArray(content.parts) ? content.parts : [];\n\n if (content.role === 'model') {\n const textPieces: string[] = [];\n const toolCalls: OpenAIToolCall[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionCall)) {\n const fc = part.functionCall as { id?: string; name: string; args?: Record<string, unknown> };\n const id = fc.id ?? generateId(fc.name);\n if (!fc.id) reporter.warn('generated-id', `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);\n toolCalls.push({\n id,\n type: 'function',\n function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) },\n });\n pending.push({ id, name: fc.name });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolCalls.length > 0) assistant.tool_calls = toolCalls;\n out.push(assistant);\n continue;\n }\n\n // role 'user' or unspecified\n const contentParts: OpenAIContentPart[] = [];\n let hasImage = false;\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionResponse)) {\n const fr = part.functionResponse as { id?: string; name: string; response?: Record<string, unknown> };\n const id = resolveResponseId(fr, pending, reporter, generateId);\n out.push({ role: 'tool', tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });\n continue;\n }\n const image = imageFromGemini(part);\n if (image) {\n contentParts.push(imageToOpenAI(image));\n hasImage = true;\n continue;\n }\n if (isRecord(part) && typeof part.text === 'string') {\n contentParts.push({ type: 'text', text: part.text });\n }\n }\n if (contentParts.length > 0) {\n if (hasImage) {\n out.push({ role: 'user', content: contentParts });\n } else {\n const text = textOf(contentParts);\n if (text) out.push({ role: 'user', content: text });\n }\n }\n }\n\n return out;\n}\n\nfunction resolveResponseId(\n response: { id?: string; name: string },\n pending: { id: string; name: string }[],\n reporter: Reporter,\n generateId: (name: string) => string,\n): string {\n if (response.id) {\n const index = pending.findIndex((p) => p.id === response.id);\n if (index >= 0) pending.splice(index, 1);\n return response.id;\n }\n const index = pending.findIndex((p) => p.name === response.name);\n if (index >= 0) {\n const { id } = pending[index];\n pending.splice(index, 1);\n return id;\n }\n const id = generateId(response.name);\n reporter.warn(\n 'unmapped-tool-result',\n `Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`,\n );\n return id;\n}\n","import type { AnthropicConversation, ConvertOptions, GeminiConversation, OpenAIMessage, Provider } from './types.js';\nimport { fromAnthropic, toAnthropic } from './providers/anthropic.js';\nimport { fromGemini, toGemini } from './providers/gemini.js';\n\n/** Maps a provider to the conversation shape it accepts and returns. */\nexport type ConversationOf<P extends Provider> = P extends 'openai'\n ? OpenAIMessage[]\n : P extends 'anthropic'\n ? AnthropicConversation\n : P extends 'gemini'\n ? GeminiConversation\n : never;\n\n/**\n * Converts a conversation from one provider format to another. Every conversion\n * routes through the canonical OpenAI representation, so any source/target pair\n * is supported, including same-provider normalization.\n *\n * @example\n * const gemini = convert(openaiMessages, { from: 'openai', to: 'gemini' });\n * const openai = convert(anthropicBody, { from: 'anthropic', to: 'openai' });\n */\nexport function convert<From extends Provider, To extends Provider>(\n conversation: ConversationOf<From>,\n route: { from: From; to: To },\n options: ConvertOptions = {},\n): ConversationOf<To> {\n const canonical = toCanonical(conversation, route.from, options);\n return fromCanonical(canonical, route.to, options) as ConversationOf<To>;\n}\n\nfunction toCanonical(conversation: unknown, from: Provider, options: ConvertOptions): OpenAIMessage[] {\n switch (from) {\n case 'openai':\n return conversation as OpenAIMessage[];\n case 'anthropic':\n return fromAnthropic(conversation as AnthropicConversation, options);\n case 'gemini':\n return fromGemini(conversation as GeminiConversation, options);\n default:\n throw new Error(`Unknown source provider: ${String(from)}`);\n }\n}\n\nfunction fromCanonical(canonical: OpenAIMessage[], to: Provider, options: ConvertOptions): ConversationOf<Provider> {\n switch (to) {\n case 'openai':\n return canonical;\n case 'anthropic':\n return toAnthropic(canonical, options);\n case 'gemini':\n return toGemini(canonical, options);\n default:\n throw new Error(`Unknown target provider: ${String(to)}`);\n }\n}\n"],"mappings":"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","names":["mergeConsecutive","index","id"]}
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,25 @@ 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
+ export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, parseDataUrl, 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,25 @@ 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
+ export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, parseDataUrl, 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;
329
+ }
330
+ const image = imageFromOpenAI(part);
331
+ if (image) {
332
+ parts.push(imageToGemini(image, reporter));
333
+ continue;
249
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
  }
@@ -382,7 +483,9 @@ export {
382
483
  convert,
383
484
  fromAnthropic,
384
485
  fromGemini,
486
+ parseDataUrl,
385
487
  toAnthropic,
488
+ toDataUrl,
386
489
  toGemini
387
490
  };
388
491
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/util.ts","../src/providers/openai.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/convert.ts"],"sourcesContent":["import type { ConvertOptions, OpenAITextPart, Warning, WarningCode } from './types.js';\n\n/** Thin wrapper around the optional `onWarning` callback. */\nexport class Reporter {\n constructor(private readonly options: ConvertOptions = {}) {}\n\n warn(code: WarningCode, message: string): void {\n this.options.onWarning?.({ code, message } satisfies Warning);\n }\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Flattens any supported text content (a plain string or an array of text\n * parts/blocks) into a single string. Non text parts are ignored.\n */\nexport function textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (isRecord(part) && typeof part.text === 'string') return part.text;\n return '';\n })\n .join('');\n }\n return '';\n}\n\n/** Wraps a string as a single OpenAI/Anthropic text part array. */\nexport function textPart(text: string): OpenAITextPart[] {\n return [{ type: 'text', text }];\n}\n\n/** Safely parses a JSON string, returning a discriminated result. */\nexport function tryParseJson(input: string): { ok: true; value: unknown } | { ok: false } {\n try {\n return { ok: true, value: JSON.parse(input) };\n } catch {\n return { ok: false };\n }\n}\n\n/**\n * Parses an OpenAI tool-call `arguments` JSON string into an object. Reports and\n * returns `{}` when the string is not valid JSON object syntax.\n */\nexport function parseArguments(args: string, reporter: Reporter, fnName: string): Record<string, unknown> {\n const parsed = tryParseJson(args);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n reporter.warn(\n 'invalid-json-arguments',\n `Tool call '${fnName}' had arguments that were not a JSON object; used an empty object instead.`,\n );\n return {};\n}\n\n/**\n * Converts an OpenAI tool result string into a Gemini `functionResponse.response`\n * object. A JSON object string is used directly; anything else is wrapped as\n * `{ result: <text> }`, which {@link unwrapResponse} reverses.\n */\nexport function wrapResponse(content: string): Record<string, unknown> {\n const parsed = tryParseJson(content);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n return { result: content };\n}\n\n/** Reverses {@link wrapResponse}: a lone `{ result: string }` becomes that string. */\nexport function unwrapResponse(response: Record<string, unknown>): string {\n const keys = Object.keys(response);\n if (keys.length === 1 && keys[0] === 'result' && typeof response.result === 'string') {\n return response.result;\n }\n return JSON.stringify(response);\n}\n","import type {\n OpenAIAssistantMessage,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIToolMessage,\n OpenAIUserMessage,\n} from '../types.js';\nimport { Reporter, textOf } from '../util.js';\n\n/** Any canonical message other than a system/developer prompt. */\nexport type NonSystemMessage = OpenAIUserMessage | OpenAIAssistantMessage | OpenAIToolMessage;\n\nfunction isSystem(message: OpenAIMessage): message is OpenAISystemMessage {\n return message.role === 'system' || message.role === 'developer';\n}\n\n/**\n * Splits a canonical OpenAI conversation into its system prompt and the\n * remaining messages. All `system` / `developer` messages are folded into a\n * single string (joined by blank lines) because Anthropic and Gemini carry the\n * system prompt as one top-level field. A system message that appears after the\n * conversation has started is still folded in, but reported as `system-midstream`.\n */\nexport function splitSystem(\n messages: OpenAIMessage[],\n reporter: Reporter,\n): { system?: string; rest: NonSystemMessage[] } {\n const systemParts: string[] = [];\n const rest: NonSystemMessage[] = [];\n let started = false;\n\n for (const message of messages) {\n if (isSystem(message)) {\n if (started) {\n reporter.warn(\n 'system-midstream',\n 'A system message appeared mid conversation; it was merged into the top-level system prompt.',\n );\n }\n systemParts.push(textOf(message.content));\n continue;\n }\n started = true;\n rest.push(message);\n }\n\n const system = systemParts.length > 0 ? systemParts.join('\\n\\n') : undefined;\n return system === undefined ? { rest } : { system, rest };\n}\n","import type {\n AnthropicContentBlock,\n AnthropicConversation,\n AnthropicMessage,\n ConvertOptions,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf } from '../util.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Anthropic */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into an Anthropic request fragment\n * (`{ system, messages }`). System messages move to the top-level `system`\n * field, tool-call arguments are JSON parsed into `input` objects, tool results\n * are folded into `tool_result` blocks inside a user turn, and consecutive\n * same-role turns are merged to satisfy Anthropic's strict alternation.\n */\nexport function toAnthropic(messages: OpenAIMessage[], options: ConvertOptions = {}): AnthropicConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n const out: AnthropicMessage[] = [];\n\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const blocks: AnthropicContentBlock[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n blocks.push({ type: 'tool_result', tool_use_id: tool.tool_call_id, content: textOf(tool.content) });\n j++;\n }\n out.push({ role: 'user', content: blocks });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n out.push({ role: 'user', content: userContent(message.content, reporter) });\n continue;\n }\n\n out.push({ role: 'assistant', content: assistantContent(message, reporter) });\n }\n\n const merged = mergeConsecutive(out, reporter);\n return system === undefined ? { messages: merged } : { system, messages: merged };\n}\n\nfunction userContent(content: string | OpenAIContentPart[], reporter: Reporter): string | AnthropicContentBlock[] {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return textOf(content);\n const blocks: AnthropicContentBlock[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n blocks.push({ type: 'text', text: part.text });\n } else {\n reporter.warn('dropped-content', 'Dropped a non-text user content part not supported by this converter.');\n }\n }\n return blocks;\n}\n\nfunction assistantContent(message: OpenAIAssistantMessage, reporter: Reporter): string | AnthropicContentBlock[] {\n const text = textOf(message.content ?? '');\n const toolCalls = message.tool_calls ?? [];\n if (toolCalls.length === 0) return text;\n\n const blocks: AnthropicContentBlock[] = [];\n if (text) blocks.push({ type: 'text', text });\n for (const call of toolCalls) {\n blocks.push({\n type: 'tool_use',\n id: call.id,\n name: call.function.name,\n input: parseArguments(call.function.arguments, reporter, call.function.name),\n });\n }\n return blocks;\n}\n\n/** Merges adjacent same-role messages by concatenating their content blocks. */\nfunction mergeConsecutive(messages: AnthropicMessage[], reporter: Reporter): AnthropicMessage[] {\n const result: AnthropicMessage[] = [];\n for (const message of messages) {\n const previous = result[result.length - 1];\n if (previous && previous.role === message.role) {\n previous.content = [...asBlocks(previous.content), ...asBlocks(message.content)];\n reporter.warn(\n 'merged-role',\n `Merged consecutive '${message.role}' turns (Anthropic requires alternating roles).`,\n );\n } else {\n result.push({ role: message.role, content: message.content });\n }\n }\n return result;\n}\n\nfunction asBlocks(content: string | AnthropicContentBlock[]): AnthropicContentBlock[] {\n if (typeof content !== 'string') return content;\n return content ? [{ type: 'text', text: content }] : [];\n}\n\n/* ------------------------------------------------------------------ */\n/* Anthropic -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts an Anthropic request fragment back into a canonical OpenAI message\n * array. The top-level `system` becomes a leading system message, `tool_use`\n * blocks become `tool_calls` (with `input` re-serialized to a JSON string), and\n * `tool_result` blocks become standalone `role: 'tool'` messages.\n */\nexport function fromAnthropic(conversation: AnthropicConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n void options;\n const out: OpenAIMessage[] = [];\n if (conversation.system) {\n out.push({ role: 'system', content: textOf(conversation.system) });\n }\n\n for (const message of conversation.messages) {\n const blocks = asBlocks(message.content);\n\n if (message.role === 'user') {\n const toolResults = blocks.filter((b) => b.type === 'tool_result');\n const textBlocks = blocks.filter((b) => b.type !== 'tool_result');\n for (const block of toolResults) {\n out.push({\n role: 'tool',\n tool_call_id: String((block as { tool_use_id?: string }).tool_use_id ?? ''),\n content: textOf((block as { content?: unknown }).content),\n });\n }\n if (textBlocks.length > 0) {\n out.push({ role: 'user', content: textOf(textBlocks) });\n }\n continue;\n }\n\n // assistant\n const text = textOf(blocks.filter((b) => b.type === 'text'));\n const toolUses = blocks.filter((b) => b.type === 'tool_use');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolUses.length > 0) {\n assistant.tool_calls = toolUses.map((block) => {\n const b = block as { id: string; name: string; input: Record<string, unknown> };\n return { id: b.id, type: 'function', function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };\n });\n }\n out.push(assistant);\n }\n\n return out;\n}\n","import type {\n ConvertOptions,\n GeminiContent,\n GeminiConversation,\n GeminiPart,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolCall,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf, unwrapResponse, wrapResponse } from '../util.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Gemini */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into a Gemini request fragment\n * (`{ systemInstruction, contents }`). The assistant role becomes `model`,\n * tool-call arguments are JSON parsed into `args` objects, tool results become\n * `functionResponse` parts whose `name` is recovered from the matching call, and\n * consecutive same-role turns are merged for Gemini's strict alternation.\n *\n * The OpenAI tool-call `id` is carried through as `functionCall.id` so that a\n * round trip (OpenAI -> Gemini -> OpenAI) preserves ids exactly.\n */\nexport function toGemini(messages: OpenAIMessage[], options: ConvertOptions = {}): GeminiConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n\n const idToName = new Map<string, string>();\n for (const message of rest) {\n if (message.role === 'assistant' && message.tool_calls) {\n for (const call of message.tool_calls) idToName.set(call.id, call.function.name);\n }\n }\n\n const contents: GeminiContent[] = [];\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const parts: GeminiPart[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n const name = idToName.get(tool.tool_call_id);\n if (!name) {\n reporter.warn(\n 'unmapped-tool-result',\n `Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`,\n );\n }\n parts.push({\n functionResponse: {\n id: tool.tool_call_id,\n name: name ?? tool.tool_call_id,\n response: wrapResponse(textOf(tool.content)),\n },\n });\n j++;\n }\n contents.push({ role: 'user', parts });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n contents.push({ role: 'user', parts: userParts(message.content, reporter) });\n continue;\n }\n\n contents.push({ role: 'model', parts: assistantParts(message, reporter) });\n }\n\n const merged = mergeConsecutive(contents, reporter);\n return system === undefined\n ? { contents: merged }\n : { systemInstruction: { parts: [{ text: system }] }, contents: merged };\n}\n\nfunction userParts(content: string | OpenAIContentPart[], reporter: Reporter): GeminiPart[] {\n if (typeof content === 'string') return [{ text: content }];\n const parts: GeminiPart[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n parts.push({ text: part.text });\n } else {\n reporter.warn('dropped-content', 'Dropped a non-text user content part not supported by this converter.');\n }\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\nfunction assistantParts(message: OpenAIAssistantMessage, reporter: Reporter): GeminiPart[] {\n const parts: GeminiPart[] = [];\n const text = textOf(message.content ?? '');\n if (text) parts.push({ text });\n for (const call of message.tool_calls ?? []) {\n parts.push({\n functionCall: {\n id: call.id,\n name: call.function.name,\n args: parseArguments(call.function.arguments, reporter, call.function.name),\n },\n });\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\n/** Merges adjacent same-role contents by concatenating their `parts` arrays. */\nfunction mergeConsecutive(contents: GeminiContent[], reporter: Reporter): GeminiContent[] {\n const result: GeminiContent[] = [];\n for (const content of contents) {\n const previous = result[result.length - 1];\n if (previous && previous.role === content.role) {\n previous.parts = [...previous.parts, ...content.parts];\n reporter.warn('merged-role', `Merged consecutive '${content.role}' turns (Gemini requires alternating roles).`);\n } else {\n result.push({ role: content.role, parts: [...content.parts] });\n }\n }\n return result;\n}\n\n/* ------------------------------------------------------------------ */\n/* Gemini -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a Gemini request fragment back into a canonical OpenAI message array.\n * Because Gemini matches tool calls and responses by function name (the `id`\n * field is optional), this maintains a queue of pending calls and resolves each\n * `functionResponse` by id when present, otherwise by name in call order,\n * generating a deterministic id as a last resort.\n */\nexport function fromGemini(conversation: GeminiConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n const reporter = new Reporter(options);\n const out: OpenAIMessage[] = [];\n\n if (conversation.systemInstruction) {\n const text = textOf(conversation.systemInstruction.parts);\n if (text) out.push({ role: 'system', content: text });\n }\n\n const pending: { id: string; name: string }[] = [];\n let counter = 0;\n const generateId = (name: string): string => `call_${name.replace(/[^a-zA-Z0-9_-]/g, '_')}_${counter++}`;\n\n for (const content of conversation.contents) {\n const parts = Array.isArray(content.parts) ? content.parts : [];\n\n if (content.role === 'model') {\n const textPieces: string[] = [];\n const toolCalls: OpenAIToolCall[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionCall)) {\n const fc = part.functionCall as { id?: string; name: string; args?: Record<string, unknown> };\n const id = fc.id ?? generateId(fc.name);\n if (!fc.id) reporter.warn('generated-id', `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);\n toolCalls.push({\n id,\n type: 'function',\n function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) },\n });\n pending.push({ id, name: fc.name });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolCalls.length > 0) assistant.tool_calls = toolCalls;\n out.push(assistant);\n continue;\n }\n\n // role 'user' or unspecified\n const textPieces: string[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionResponse)) {\n const fr = part.functionResponse as { id?: string; name: string; response?: Record<string, unknown> };\n const id = resolveResponseId(fr, pending, reporter, generateId);\n out.push({ role: 'tool', tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n if (text) out.push({ role: 'user', content: text });\n }\n\n return out;\n}\n\nfunction resolveResponseId(\n response: { id?: string; name: string },\n pending: { id: string; name: string }[],\n reporter: Reporter,\n generateId: (name: string) => string,\n): string {\n if (response.id) {\n const index = pending.findIndex((p) => p.id === response.id);\n if (index >= 0) pending.splice(index, 1);\n return response.id;\n }\n const index = pending.findIndex((p) => p.name === response.name);\n if (index >= 0) {\n const { id } = pending[index];\n pending.splice(index, 1);\n return id;\n }\n const id = generateId(response.name);\n reporter.warn(\n 'unmapped-tool-result',\n `Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`,\n );\n return id;\n}\n","import type { AnthropicConversation, ConvertOptions, GeminiConversation, OpenAIMessage, Provider } from './types.js';\nimport { fromAnthropic, toAnthropic } from './providers/anthropic.js';\nimport { fromGemini, toGemini } from './providers/gemini.js';\n\n/** Maps a provider to the conversation shape it accepts and returns. */\nexport type ConversationOf<P extends Provider> = P extends 'openai'\n ? OpenAIMessage[]\n : P extends 'anthropic'\n ? AnthropicConversation\n : P extends 'gemini'\n ? GeminiConversation\n : never;\n\n/**\n * Converts a conversation from one provider format to another. Every conversion\n * routes through the canonical OpenAI representation, so any source/target pair\n * is supported, including same-provider normalization.\n *\n * @example\n * const gemini = convert(openaiMessages, { from: 'openai', to: 'gemini' });\n * const openai = convert(anthropicBody, { from: 'anthropic', to: 'openai' });\n */\nexport function convert<From extends Provider, To extends Provider>(\n conversation: ConversationOf<From>,\n route: { from: From; to: To },\n options: ConvertOptions = {},\n): ConversationOf<To> {\n const canonical = toCanonical(conversation, route.from, options);\n return fromCanonical(canonical, route.to, options) as ConversationOf<To>;\n}\n\nfunction toCanonical(conversation: unknown, from: Provider, options: ConvertOptions): OpenAIMessage[] {\n switch (from) {\n case 'openai':\n return conversation as OpenAIMessage[];\n case 'anthropic':\n return fromAnthropic(conversation as AnthropicConversation, options);\n case 'gemini':\n return fromGemini(conversation as GeminiConversation, options);\n default:\n throw new Error(`Unknown source provider: ${String(from)}`);\n }\n}\n\nfunction fromCanonical(canonical: OpenAIMessage[], to: Provider, options: ConvertOptions): ConversationOf<Provider> {\n switch (to) {\n case 'openai':\n return canonical;\n case 'anthropic':\n return toAnthropic(canonical, options);\n case 'gemini':\n return toGemini(canonical, options);\n default:\n throw new Error(`Unknown target provider: ${String(to)}`);\n }\n}\n"],"mappings":"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","names":["mergeConsecutive","textPieces","text","index","id"]}
1
+ {"version":3,"sources":["../src/util.ts","../src/image.ts","../src/providers/openai.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/convert.ts"],"sourcesContent":["import type { ConvertOptions, OpenAITextPart, Warning, WarningCode } from './types.js';\n\n/** Thin wrapper around the optional `onWarning` callback. */\nexport class Reporter {\n constructor(private readonly options: ConvertOptions = {}) {}\n\n warn(code: WarningCode, message: string): void {\n this.options.onWarning?.({ code, message } satisfies Warning);\n }\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Flattens any supported text content (a plain string or an array of text\n * parts/blocks) into a single string. Non text parts are ignored.\n */\nexport function textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (isRecord(part) && typeof part.text === 'string') return part.text;\n return '';\n })\n .join('');\n }\n return '';\n}\n\n/** Wraps a string as a single OpenAI/Anthropic text part array. */\nexport function textPart(text: string): OpenAITextPart[] {\n return [{ type: 'text', text }];\n}\n\n/** Safely parses a JSON string, returning a discriminated result. */\nexport function tryParseJson(input: string): { ok: true; value: unknown } | { ok: false } {\n try {\n return { ok: true, value: JSON.parse(input) };\n } catch {\n return { ok: false };\n }\n}\n\n/**\n * Parses an OpenAI tool-call `arguments` JSON string into an object. Reports and\n * returns `{}` when the string is not valid JSON object syntax.\n */\nexport function parseArguments(args: string, reporter: Reporter, fnName: string): Record<string, unknown> {\n const parsed = tryParseJson(args);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n reporter.warn(\n 'invalid-json-arguments',\n `Tool call '${fnName}' had arguments that were not a JSON object; used an empty object instead.`,\n );\n return {};\n}\n\n/**\n * Converts an OpenAI tool result string into a Gemini `functionResponse.response`\n * object. A JSON object string is used directly; anything else is wrapped as\n * `{ result: <text> }`, which {@link unwrapResponse} reverses.\n */\nexport function wrapResponse(content: string): Record<string, unknown> {\n const parsed = tryParseJson(content);\n if (parsed.ok && isRecord(parsed.value)) return parsed.value;\n return { result: content };\n}\n\n/** Reverses {@link wrapResponse}: a lone `{ result: string }` becomes that string. */\nexport function unwrapResponse(response: Record<string, unknown>): string {\n const keys = Object.keys(response);\n if (keys.length === 1 && keys[0] === 'result' && typeof response.result === 'string') {\n return response.result;\n }\n return JSON.stringify(response);\n}\n","import type { AnthropicImageBlock, GeminiPart, OpenAIImagePart } from './types.js';\nimport { Reporter, isRecord } from './util.js';\n\n/**\n * A provider-neutral image. `base64` carries the raw bytes plus media type\n * (mapping to a data URL, an Anthropic base64 source or a Gemini `inlineData`\n * part); `url` carries a remote reference.\n */\nexport type NormalizedImage = { kind: 'base64'; mediaType: string; data: string } | { kind: 'url'; url: string };\n\nconst DATA_URL = /^data:([^;,]+);base64,(.*)$/s;\n\n/** Decomposes a `data:<mediaType>;base64,<data>` URL. Returns null otherwise. */\nexport function parseDataUrl(url: string): { mediaType: string; data: string } | null {\n const match = DATA_URL.exec(url);\n if (!match) return null;\n return { mediaType: match[1], data: match[2] };\n}\n\n/** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */\nexport function toDataUrl(mediaType: string, data: string): string {\n return `data:${mediaType};base64,${data}`;\n}\n\n/* ------------------------------- OpenAI -------------------------------- */\n\nexport function imageFromOpenAI(part: unknown): NormalizedImage | null {\n if (!isRecord(part) || part.type !== 'image_url' || !isRecord(part.image_url)) return null;\n const url = part.image_url.url;\n if (typeof url !== 'string') return null;\n const parsed = parseDataUrl(url);\n return parsed ? { kind: 'base64', ...parsed } : { kind: 'url', url };\n}\n\nexport function imageToOpenAI(image: NormalizedImage): OpenAIImagePart {\n const url = image.kind === 'base64' ? toDataUrl(image.mediaType, image.data) : image.url;\n return { type: 'image_url', image_url: { url } };\n}\n\n/* ------------------------------ Anthropic ------------------------------ */\n\nexport function imageFromAnthropic(block: unknown): NormalizedImage | null {\n if (!isRecord(block) || block.type !== 'image' || !isRecord(block.source)) return null;\n const source = block.source;\n if (source.type === 'base64' && typeof source.media_type === 'string' && typeof source.data === 'string') {\n return { kind: 'base64', mediaType: source.media_type, data: source.data };\n }\n if (source.type === 'url' && typeof source.url === 'string') {\n return { kind: 'url', url: source.url };\n }\n return null;\n}\n\nexport function imageToAnthropic(image: NormalizedImage): AnthropicImageBlock {\n if (image.kind === 'base64') {\n return { type: 'image', source: { type: 'base64', media_type: image.mediaType, data: image.data } };\n }\n return { type: 'image', source: { type: 'url', url: image.url } };\n}\n\n/* -------------------------------- Gemini ------------------------------- */\n\nexport function imageFromGemini(part: unknown): NormalizedImage | null {\n if (isRecord(part) && isRecord(part.inlineData)) {\n const data = part.inlineData;\n if (typeof data.mimeType === 'string' && typeof data.data === 'string') {\n return { kind: 'base64', mediaType: data.mimeType, data: data.data };\n }\n }\n if (isRecord(part) && isRecord(part.fileData)) {\n const data = part.fileData;\n if (typeof data.fileUri === 'string') return { kind: 'url', url: data.fileUri };\n }\n return null;\n}\n\nexport function imageToGemini(image: NormalizedImage, reporter: Reporter): GeminiPart {\n if (image.kind === 'base64') {\n return { inlineData: { mimeType: image.mediaType, data: image.data } };\n }\n // Gemini's fileData.fileUri is intended for Files API URIs, not arbitrary\n // public URLs. We emit it as a best effort and flag it so the caller can\n // upload via the Files API or inline the bytes if Gemini rejects it.\n reporter.warn(\n 'gemini-url-image',\n 'A remote image URL was emitted as Gemini fileData.fileUri; Gemini may require the Files API for non-Google URIs.',\n );\n return { fileData: { fileUri: image.url } };\n}\n","import type {\n OpenAIAssistantMessage,\n OpenAIMessage,\n OpenAISystemMessage,\n OpenAIToolMessage,\n OpenAIUserMessage,\n} from '../types.js';\nimport { Reporter, textOf } from '../util.js';\n\n/** Any canonical message other than a system/developer prompt. */\nexport type NonSystemMessage = OpenAIUserMessage | OpenAIAssistantMessage | OpenAIToolMessage;\n\nfunction isSystem(message: OpenAIMessage): message is OpenAISystemMessage {\n return message.role === 'system' || message.role === 'developer';\n}\n\n/**\n * Splits a canonical OpenAI conversation into its system prompt and the\n * remaining messages. All `system` / `developer` messages are folded into a\n * single string (joined by blank lines) because Anthropic and Gemini carry the\n * system prompt as one top-level field. A system message that appears after the\n * conversation has started is still folded in, but reported as `system-midstream`.\n */\nexport function splitSystem(\n messages: OpenAIMessage[],\n reporter: Reporter,\n): { system?: string; rest: NonSystemMessage[] } {\n const systemParts: string[] = [];\n const rest: NonSystemMessage[] = [];\n let started = false;\n\n for (const message of messages) {\n if (isSystem(message)) {\n if (started) {\n reporter.warn(\n 'system-midstream',\n 'A system message appeared mid conversation; it was merged into the top-level system prompt.',\n );\n }\n systemParts.push(textOf(message.content));\n continue;\n }\n started = true;\n rest.push(message);\n }\n\n const system = systemParts.length > 0 ? systemParts.join('\\n\\n') : undefined;\n return system === undefined ? { rest } : { system, rest };\n}\n","import type {\n AnthropicContentBlock,\n AnthropicConversation,\n AnthropicMessage,\n ConvertOptions,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf } from '../util.js';\nimport { imageFromAnthropic, imageFromOpenAI, imageToAnthropic, imageToOpenAI } from '../image.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Anthropic */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into an Anthropic request fragment\n * (`{ system, messages }`). System messages move to the top-level `system`\n * field, tool-call arguments are JSON parsed into `input` objects, tool results\n * are folded into `tool_result` blocks inside a user turn, and consecutive\n * same-role turns are merged to satisfy Anthropic's strict alternation.\n */\nexport function toAnthropic(messages: OpenAIMessage[], options: ConvertOptions = {}): AnthropicConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n const out: AnthropicMessage[] = [];\n\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const blocks: AnthropicContentBlock[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n blocks.push({ type: 'tool_result', tool_use_id: tool.tool_call_id, content: textOf(tool.content) });\n j++;\n }\n out.push({ role: 'user', content: blocks });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n out.push({ role: 'user', content: userContent(message.content, reporter) });\n continue;\n }\n\n out.push({ role: 'assistant', content: assistantContent(message, reporter) });\n }\n\n const merged = mergeConsecutive(out, reporter);\n return system === undefined ? { messages: merged } : { system, messages: merged };\n}\n\nfunction userContent(content: string | OpenAIContentPart[], reporter: Reporter): string | AnthropicContentBlock[] {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return textOf(content);\n const blocks: AnthropicContentBlock[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n blocks.push({ type: 'text', text: part.text });\n continue;\n }\n const image = imageFromOpenAI(part);\n if (image) {\n blocks.push(imageToAnthropic(image));\n continue;\n }\n reporter.warn('dropped-content', 'Dropped an unsupported user content part.');\n }\n return blocks;\n}\n\nfunction assistantContent(message: OpenAIAssistantMessage, reporter: Reporter): string | AnthropicContentBlock[] {\n const text = textOf(message.content ?? '');\n const toolCalls = message.tool_calls ?? [];\n if (toolCalls.length === 0) return text;\n\n const blocks: AnthropicContentBlock[] = [];\n if (text) blocks.push({ type: 'text', text });\n for (const call of toolCalls) {\n blocks.push({\n type: 'tool_use',\n id: call.id,\n name: call.function.name,\n input: parseArguments(call.function.arguments, reporter, call.function.name),\n });\n }\n return blocks;\n}\n\n/** Merges adjacent same-role messages by concatenating their content blocks. */\nfunction mergeConsecutive(messages: AnthropicMessage[], reporter: Reporter): AnthropicMessage[] {\n const result: AnthropicMessage[] = [];\n for (const message of messages) {\n const previous = result[result.length - 1];\n if (previous && previous.role === message.role) {\n previous.content = [...asBlocks(previous.content), ...asBlocks(message.content)];\n reporter.warn(\n 'merged-role',\n `Merged consecutive '${message.role}' turns (Anthropic requires alternating roles).`,\n );\n } else {\n result.push({ role: message.role, content: message.content });\n }\n }\n return result;\n}\n\nfunction asBlocks(content: string | AnthropicContentBlock[]): AnthropicContentBlock[] {\n if (typeof content !== 'string') return content;\n return content ? [{ type: 'text', text: content }] : [];\n}\n\n/* ------------------------------------------------------------------ */\n/* Anthropic -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts an Anthropic request fragment back into a canonical OpenAI message\n * array. The top-level `system` becomes a leading system message, `tool_use`\n * blocks become `tool_calls` (with `input` re-serialized to a JSON string), and\n * `tool_result` blocks become standalone `role: 'tool'` messages.\n */\nexport function fromAnthropic(conversation: AnthropicConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n void options;\n const out: OpenAIMessage[] = [];\n if (conversation.system) {\n out.push({ role: 'system', content: textOf(conversation.system) });\n }\n\n for (const message of conversation.messages) {\n const blocks = asBlocks(message.content);\n\n if (message.role === 'user') {\n const toolResults = blocks.filter((b) => b.type === 'tool_result');\n const contentBlocks = blocks.filter((b) => b.type !== 'tool_result');\n for (const block of toolResults) {\n out.push({\n role: 'tool',\n tool_call_id: String((block as { tool_use_id?: string }).tool_use_id ?? ''),\n content: textOf((block as { content?: unknown }).content),\n });\n }\n if (contentBlocks.length > 0) {\n out.push({ role: 'user', content: userContentToOpenAI(contentBlocks) });\n }\n continue;\n }\n\n // assistant\n const text = textOf(blocks.filter((b) => b.type === 'text'));\n const toolUses = blocks.filter((b) => b.type === 'tool_use');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolUses.length > 0) {\n assistant.tool_calls = toolUses.map((block) => {\n const b = block as { id: string; name: string; input: Record<string, unknown> };\n return { id: b.id, type: 'function', function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };\n });\n }\n out.push(assistant);\n }\n\n return out;\n}\n\n/** Rebuilds OpenAI user content from Anthropic blocks, as a string unless an image is present. */\nfunction userContentToOpenAI(blocks: AnthropicContentBlock[]): string | OpenAIContentPart[] {\n const hasImage = blocks.some((block) => imageFromAnthropic(block) !== null);\n if (!hasImage) return textOf(blocks);\n const parts: OpenAIContentPart[] = [];\n for (const block of blocks) {\n const image = imageFromAnthropic(block);\n if (image) {\n parts.push(imageToOpenAI(image));\n } else if (isRecord(block) && block.type === 'text' && typeof block.text === 'string') {\n parts.push({ type: 'text', text: block.text });\n }\n }\n return parts;\n}\n","import type {\n ConvertOptions,\n GeminiContent,\n GeminiConversation,\n GeminiPart,\n OpenAIAssistantMessage,\n OpenAIContentPart,\n OpenAIMessage,\n OpenAIToolCall,\n OpenAIToolMessage,\n} from '../types.js';\nimport { Reporter, isRecord, parseArguments, textOf, unwrapResponse, wrapResponse } from '../util.js';\nimport { imageFromGemini, imageFromOpenAI, imageToGemini, imageToOpenAI } from '../image.js';\nimport { splitSystem } from './openai.js';\n\n/* ------------------------------------------------------------------ */\n/* OpenAI (canonical) -> Gemini */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a canonical OpenAI conversation into a Gemini request fragment\n * (`{ systemInstruction, contents }`). The assistant role becomes `model`,\n * tool-call arguments are JSON parsed into `args` objects, tool results become\n * `functionResponse` parts whose `name` is recovered from the matching call, and\n * consecutive same-role turns are merged for Gemini's strict alternation.\n *\n * The OpenAI tool-call `id` is carried through as `functionCall.id` so that a\n * round trip (OpenAI -> Gemini -> OpenAI) preserves ids exactly.\n */\nexport function toGemini(messages: OpenAIMessage[], options: ConvertOptions = {}): GeminiConversation {\n const reporter = new Reporter(options);\n const { system, rest } = splitSystem(messages, reporter);\n\n const idToName = new Map<string, string>();\n for (const message of rest) {\n if (message.role === 'assistant' && message.tool_calls) {\n for (const call of message.tool_calls) idToName.set(call.id, call.function.name);\n }\n }\n\n const contents: GeminiContent[] = [];\n for (let i = 0; i < rest.length; i++) {\n const message = rest[i];\n\n if (message.role === 'tool') {\n const parts: GeminiPart[] = [];\n let j = i;\n while (j < rest.length && rest[j].role === 'tool') {\n const tool = rest[j] as OpenAIToolMessage;\n const name = idToName.get(tool.tool_call_id);\n if (!name) {\n reporter.warn(\n 'unmapped-tool-result',\n `Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`,\n );\n }\n parts.push({\n functionResponse: {\n id: tool.tool_call_id,\n name: name ?? tool.tool_call_id,\n response: wrapResponse(textOf(tool.content)),\n },\n });\n j++;\n }\n contents.push({ role: 'user', parts });\n i = j - 1;\n continue;\n }\n\n if (message.role === 'user') {\n contents.push({ role: 'user', parts: userParts(message.content, reporter) });\n continue;\n }\n\n contents.push({ role: 'model', parts: assistantParts(message, reporter) });\n }\n\n const merged = mergeConsecutive(contents, reporter);\n return system === undefined\n ? { contents: merged }\n : { systemInstruction: { parts: [{ text: system }] }, contents: merged };\n}\n\nfunction userParts(content: string | OpenAIContentPart[], reporter: Reporter): GeminiPart[] {\n if (typeof content === 'string') return [{ text: content }];\n const parts: GeminiPart[] = [];\n for (const part of content) {\n if (isRecord(part) && part.type === 'text' && typeof part.text === 'string') {\n parts.push({ text: part.text });\n continue;\n }\n const image = imageFromOpenAI(part);\n if (image) {\n parts.push(imageToGemini(image, reporter));\n continue;\n }\n reporter.warn('dropped-content', 'Dropped an unsupported user content part.');\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\nfunction assistantParts(message: OpenAIAssistantMessage, reporter: Reporter): GeminiPart[] {\n const parts: GeminiPart[] = [];\n const text = textOf(message.content ?? '');\n if (text) parts.push({ text });\n for (const call of message.tool_calls ?? []) {\n parts.push({\n functionCall: {\n id: call.id,\n name: call.function.name,\n args: parseArguments(call.function.arguments, reporter, call.function.name),\n },\n });\n }\n return parts.length > 0 ? parts : [{ text: '' }];\n}\n\n/** Merges adjacent same-role contents by concatenating their `parts` arrays. */\nfunction mergeConsecutive(contents: GeminiContent[], reporter: Reporter): GeminiContent[] {\n const result: GeminiContent[] = [];\n for (const content of contents) {\n const previous = result[result.length - 1];\n if (previous && previous.role === content.role) {\n previous.parts = [...previous.parts, ...content.parts];\n reporter.warn('merged-role', `Merged consecutive '${content.role}' turns (Gemini requires alternating roles).`);\n } else {\n result.push({ role: content.role, parts: [...content.parts] });\n }\n }\n return result;\n}\n\n/* ------------------------------------------------------------------ */\n/* Gemini -> OpenAI (canonical) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Converts a Gemini request fragment back into a canonical OpenAI message array.\n * Because Gemini matches tool calls and responses by function name (the `id`\n * field is optional), this maintains a queue of pending calls and resolves each\n * `functionResponse` by id when present, otherwise by name in call order,\n * generating a deterministic id as a last resort.\n */\nexport function fromGemini(conversation: GeminiConversation, options: ConvertOptions = {}): OpenAIMessage[] {\n const reporter = new Reporter(options);\n const out: OpenAIMessage[] = [];\n\n if (conversation.systemInstruction) {\n const text = textOf(conversation.systemInstruction.parts);\n if (text) out.push({ role: 'system', content: text });\n }\n\n const pending: { id: string; name: string }[] = [];\n let counter = 0;\n const generateId = (name: string): string => `call_${name.replace(/[^a-zA-Z0-9_-]/g, '_')}_${counter++}`;\n\n for (const content of conversation.contents) {\n const parts = Array.isArray(content.parts) ? content.parts : [];\n\n if (content.role === 'model') {\n const textPieces: string[] = [];\n const toolCalls: OpenAIToolCall[] = [];\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionCall)) {\n const fc = part.functionCall as { id?: string; name: string; args?: Record<string, unknown> };\n const id = fc.id ?? generateId(fc.name);\n if (!fc.id) reporter.warn('generated-id', `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);\n toolCalls.push({\n id,\n type: 'function',\n function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) },\n });\n pending.push({ id, name: fc.name });\n } else if (isRecord(part) && typeof part.text === 'string') {\n textPieces.push(part.text);\n }\n }\n const text = textPieces.join('');\n const assistant: OpenAIAssistantMessage = { role: 'assistant', content: text || null };\n if (toolCalls.length > 0) assistant.tool_calls = toolCalls;\n out.push(assistant);\n continue;\n }\n\n // role 'user' or unspecified\n const contentParts: OpenAIContentPart[] = [];\n let hasImage = false;\n for (const part of parts) {\n if (isRecord(part) && isRecord(part.functionResponse)) {\n const fr = part.functionResponse as { id?: string; name: string; response?: Record<string, unknown> };\n const id = resolveResponseId(fr, pending, reporter, generateId);\n out.push({ role: 'tool', tool_call_id: id, content: unwrapResponse(fr.response ?? {}) });\n continue;\n }\n const image = imageFromGemini(part);\n if (image) {\n contentParts.push(imageToOpenAI(image));\n hasImage = true;\n continue;\n }\n if (isRecord(part) && typeof part.text === 'string') {\n contentParts.push({ type: 'text', text: part.text });\n }\n }\n if (contentParts.length > 0) {\n if (hasImage) {\n out.push({ role: 'user', content: contentParts });\n } else {\n const text = textOf(contentParts);\n if (text) out.push({ role: 'user', content: text });\n }\n }\n }\n\n return out;\n}\n\nfunction resolveResponseId(\n response: { id?: string; name: string },\n pending: { id: string; name: string }[],\n reporter: Reporter,\n generateId: (name: string) => string,\n): string {\n if (response.id) {\n const index = pending.findIndex((p) => p.id === response.id);\n if (index >= 0) pending.splice(index, 1);\n return response.id;\n }\n const index = pending.findIndex((p) => p.name === response.name);\n if (index >= 0) {\n const { id } = pending[index];\n pending.splice(index, 1);\n return id;\n }\n const id = generateId(response.name);\n reporter.warn(\n 'unmapped-tool-result',\n `Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`,\n );\n return id;\n}\n","import type { AnthropicConversation, ConvertOptions, GeminiConversation, OpenAIMessage, Provider } from './types.js';\nimport { fromAnthropic, toAnthropic } from './providers/anthropic.js';\nimport { fromGemini, toGemini } from './providers/gemini.js';\n\n/** Maps a provider to the conversation shape it accepts and returns. */\nexport type ConversationOf<P extends Provider> = P extends 'openai'\n ? OpenAIMessage[]\n : P extends 'anthropic'\n ? AnthropicConversation\n : P extends 'gemini'\n ? GeminiConversation\n : never;\n\n/**\n * Converts a conversation from one provider format to another. Every conversion\n * routes through the canonical OpenAI representation, so any source/target pair\n * is supported, including same-provider normalization.\n *\n * @example\n * const gemini = convert(openaiMessages, { from: 'openai', to: 'gemini' });\n * const openai = convert(anthropicBody, { from: 'anthropic', to: 'openai' });\n */\nexport function convert<From extends Provider, To extends Provider>(\n conversation: ConversationOf<From>,\n route: { from: From; to: To },\n options: ConvertOptions = {},\n): ConversationOf<To> {\n const canonical = toCanonical(conversation, route.from, options);\n return fromCanonical(canonical, route.to, options) as ConversationOf<To>;\n}\n\nfunction toCanonical(conversation: unknown, from: Provider, options: ConvertOptions): OpenAIMessage[] {\n switch (from) {\n case 'openai':\n return conversation as OpenAIMessage[];\n case 'anthropic':\n return fromAnthropic(conversation as AnthropicConversation, options);\n case 'gemini':\n return fromGemini(conversation as GeminiConversation, options);\n default:\n throw new Error(`Unknown source provider: ${String(from)}`);\n }\n}\n\nfunction fromCanonical(canonical: OpenAIMessage[], to: Provider, options: ConvertOptions): ConversationOf<Provider> {\n switch (to) {\n case 'openai':\n return canonical;\n case 'anthropic':\n return toAnthropic(canonical, options);\n case 'gemini':\n return toGemini(canonical, options);\n default:\n throw new Error(`Unknown target provider: ${String(to)}`);\n }\n}\n"],"mappings":";AAGO,IAAM,WAAN,MAAe;AAAA,EACpB,YAA6B,UAA0B,CAAC,GAAG;AAA9B;AAAA,EAA+B;AAAA,EAA/B;AAAA,EAE7B,KAAK,MAAmB,SAAuB;AAC7C,SAAK,QAAQ,YAAY,EAAE,MAAM,QAAQ,CAAmB;AAAA,EAC9D;AACF;AAEO,SAAS,SAAS,OAAkD;AACzE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAMO,SAAS,OAAO,SAA0B;AAC/C,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,WAAO,QACJ,IAAI,CAAC,SAAS;AACb,UAAI,OAAO,SAAS,SAAU,QAAO;AACrC,UAAI,SAAS,IAAI,KAAK,OAAO,KAAK,SAAS,SAAU,QAAO,KAAK;AACjE,aAAO;AAAA,IACT,CAAC,EACA,KAAK,EAAE;AAAA,EACZ;AACA,SAAO;AACT;AAQO,SAAS,aAAa,OAA6D;AACxF,MAAI;AACF,WAAO,EAAE,IAAI,MAAM,OAAO,KAAK,MAAM,KAAK,EAAE;AAAA,EAC9C,QAAQ;AACN,WAAO,EAAE,IAAI,MAAM;AAAA,EACrB;AACF;AAMO,SAAS,eAAe,MAAc,UAAoB,QAAyC;AACxG,QAAM,SAAS,aAAa,IAAI;AAChC,MAAI,OAAO,MAAM,SAAS,OAAO,KAAK,EAAG,QAAO,OAAO;AACvD,WAAS;AAAA,IACP;AAAA,IACA,cAAc,MAAM;AAAA,EACtB;AACA,SAAO,CAAC;AACV;AAOO,SAAS,aAAa,SAA0C;AACrE,QAAM,SAAS,aAAa,OAAO;AACnC,MAAI,OAAO,MAAM,SAAS,OAAO,KAAK,EAAG,QAAO,OAAO;AACvD,SAAO,EAAE,QAAQ,QAAQ;AAC3B;AAGO,SAAS,eAAe,UAA2C;AACxE,QAAM,OAAO,OAAO,KAAK,QAAQ;AACjC,MAAI,KAAK,WAAW,KAAK,KAAK,CAAC,MAAM,YAAY,OAAO,SAAS,WAAW,UAAU;AACpF,WAAO,SAAS;AAAA,EAClB;AACA,SAAO,KAAK,UAAU,QAAQ;AAChC;;;ACrEA,IAAM,WAAW;AAGV,SAAS,aAAa,KAAyD;AACpF,QAAM,QAAQ,SAAS,KAAK,GAAG;AAC/B,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,EAAE,WAAW,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,EAAE;AAC/C;AAGO,SAAS,UAAU,WAAmB,MAAsB;AACjE,SAAO,QAAQ,SAAS,WAAW,IAAI;AACzC;AAIO,SAAS,gBAAgB,MAAuC;AACrE,MAAI,CAAC,SAAS,IAAI,KAAK,KAAK,SAAS,eAAe,CAAC,SAAS,KAAK,SAAS,EAAG,QAAO;AACtF,QAAM,MAAM,KAAK,UAAU;AAC3B,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAM,SAAS,aAAa,GAAG;AAC/B,SAAO,SAAS,EAAE,MAAM,UAAU,GAAG,OAAO,IAAI,EAAE,MAAM,OAAO,IAAI;AACrE;AAEO,SAAS,cAAc,OAAyC;AACrE,QAAM,MAAM,MAAM,SAAS,WAAW,UAAU,MAAM,WAAW,MAAM,IAAI,IAAI,MAAM;AACrF,SAAO,EAAE,MAAM,aAAa,WAAW,EAAE,IAAI,EAAE;AACjD;AAIO,SAAS,mBAAmB,OAAwC;AACzE,MAAI,CAAC,SAAS,KAAK,KAAK,MAAM,SAAS,WAAW,CAAC,SAAS,MAAM,MAAM,EAAG,QAAO;AAClF,QAAM,SAAS,MAAM;AACrB,MAAI,OAAO,SAAS,YAAY,OAAO,OAAO,eAAe,YAAY,OAAO,OAAO,SAAS,UAAU;AACxG,WAAO,EAAE,MAAM,UAAU,WAAW,OAAO,YAAY,MAAM,OAAO,KAAK;AAAA,EAC3E;AACA,MAAI,OAAO,SAAS,SAAS,OAAO,OAAO,QAAQ,UAAU;AAC3D,WAAO,EAAE,MAAM,OAAO,KAAK,OAAO,IAAI;AAAA,EACxC;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,OAA6C;AAC5E,MAAI,MAAM,SAAS,UAAU;AAC3B,WAAO,EAAE,MAAM,SAAS,QAAQ,EAAE,MAAM,UAAU,YAAY,MAAM,WAAW,MAAM,MAAM,KAAK,EAAE;AAAA,EACpG;AACA,SAAO,EAAE,MAAM,SAAS,QAAQ,EAAE,MAAM,OAAO,KAAK,MAAM,IAAI,EAAE;AAClE;AAIO,SAAS,gBAAgB,MAAuC;AACrE,MAAI,SAAS,IAAI,KAAK,SAAS,KAAK,UAAU,GAAG;AAC/C,UAAM,OAAO,KAAK;AAClB,QAAI,OAAO,KAAK,aAAa,YAAY,OAAO,KAAK,SAAS,UAAU;AACtE,aAAO,EAAE,MAAM,UAAU,WAAW,KAAK,UAAU,MAAM,KAAK,KAAK;AAAA,IACrE;AAAA,EACF;AACA,MAAI,SAAS,IAAI,KAAK,SAAS,KAAK,QAAQ,GAAG;AAC7C,UAAM,OAAO,KAAK;AAClB,QAAI,OAAO,KAAK,YAAY,SAAU,QAAO,EAAE,MAAM,OAAO,KAAK,KAAK,QAAQ;AAAA,EAChF;AACA,SAAO;AACT;AAEO,SAAS,cAAc,OAAwB,UAAgC;AACpF,MAAI,MAAM,SAAS,UAAU;AAC3B,WAAO,EAAE,YAAY,EAAE,UAAU,MAAM,WAAW,MAAM,MAAM,KAAK,EAAE;AAAA,EACvE;AAIA,WAAS;AAAA,IACP;AAAA,IACA;AAAA,EACF;AACA,SAAO,EAAE,UAAU,EAAE,SAAS,MAAM,IAAI,EAAE;AAC5C;;;AC5EA,SAAS,SAAS,SAAwD;AACxE,SAAO,QAAQ,SAAS,YAAY,QAAQ,SAAS;AACvD;AASO,SAAS,YACd,UACA,UAC+C;AAC/C,QAAM,cAAwB,CAAC;AAC/B,QAAM,OAA2B,CAAC;AAClC,MAAI,UAAU;AAEd,aAAW,WAAW,UAAU;AAC9B,QAAI,SAAS,OAAO,GAAG;AACrB,UAAI,SAAS;AACX,iBAAS;AAAA,UACP;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA,kBAAY,KAAK,OAAO,QAAQ,OAAO,CAAC;AACxC;AAAA,IACF;AACA,cAAU;AACV,SAAK,KAAK,OAAO;AAAA,EACnB;AAEA,QAAM,SAAS,YAAY,SAAS,IAAI,YAAY,KAAK,MAAM,IAAI;AACnE,SAAO,WAAW,SAAY,EAAE,KAAK,IAAI,EAAE,QAAQ,KAAK;AAC1D;;;ACvBO,SAAS,YAAY,UAA2B,UAA0B,CAAC,GAA0B;AAC1G,QAAM,WAAW,IAAI,SAAS,OAAO;AACrC,QAAM,EAAE,QAAQ,KAAK,IAAI,YAAY,UAAU,QAAQ;AACvD,QAAM,MAA0B,CAAC;AAEjC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,UAAU,KAAK,CAAC;AAEtB,QAAI,QAAQ,SAAS,QAAQ;AAC3B,YAAM,SAAkC,CAAC;AACzC,UAAI,IAAI;AACR,aAAO,IAAI,KAAK,UAAU,KAAK,CAAC,EAAE,SAAS,QAAQ;AACjD,cAAM,OAAO,KAAK,CAAC;AACnB,eAAO,KAAK,EAAE,MAAM,eAAe,aAAa,KAAK,cAAc,SAAS,OAAO,KAAK,OAAO,EAAE,CAAC;AAClG;AAAA,MACF;AACA,UAAI,KAAK,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC;AAC1C,UAAI,IAAI;AACR;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAI,KAAK,EAAE,MAAM,QAAQ,SAAS,YAAY,QAAQ,SAAS,QAAQ,EAAE,CAAC;AAC1E;AAAA,IACF;AAEA,QAAI,KAAK,EAAE,MAAM,aAAa,SAAS,iBAAiB,SAAS,QAAQ,EAAE,CAAC;AAAA,EAC9E;AAEA,QAAM,SAAS,iBAAiB,KAAK,QAAQ;AAC7C,SAAO,WAAW,SAAY,EAAE,UAAU,OAAO,IAAI,EAAE,QAAQ,UAAU,OAAO;AAClF;AAEA,SAAS,YAAY,SAAuC,UAAsD;AAChH,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO,OAAO,OAAO;AAClD,QAAM,SAAkC,CAAC;AACzC,aAAW,QAAQ,SAAS;AAC1B,QAAI,SAAS,IAAI,KAAK,KAAK,SAAS,UAAU,OAAO,KAAK,SAAS,UAAU;AAC3E,aAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,KAAK,KAAK,CAAC;AAC7C;AAAA,IACF;AACA,UAAM,QAAQ,gBAAgB,IAAI;AAClC,QAAI,OAAO;AACT,aAAO,KAAK,iBAAiB,KAAK,CAAC;AACnC;AAAA,IACF;AACA,aAAS,KAAK,mBAAmB,2CAA2C;AAAA,EAC9E;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAAiC,UAAsD;AAC/G,QAAM,OAAO,OAAO,QAAQ,WAAW,EAAE;AACzC,QAAM,YAAY,QAAQ,cAAc,CAAC;AACzC,MAAI,UAAU,WAAW,EAAG,QAAO;AAEnC,QAAM,SAAkC,CAAC;AACzC,MAAI,KAAM,QAAO,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;AAC5C,aAAW,QAAQ,WAAW;AAC5B,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,IAAI,KAAK;AAAA,MACT,MAAM,KAAK,SAAS;AAAA,MACpB,OAAO,eAAe,KAAK,SAAS,WAAW,UAAU,KAAK,SAAS,IAAI;AAAA,IAC7E,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAGA,SAAS,iBAAiB,UAA8B,UAAwC;AAC9F,QAAM,SAA6B,CAAC;AACpC,aAAW,WAAW,UAAU;AAC9B,UAAM,WAAW,OAAO,OAAO,SAAS,CAAC;AACzC,QAAI,YAAY,SAAS,SAAS,QAAQ,MAAM;AAC9C,eAAS,UAAU,CAAC,GAAG,SAAS,SAAS,OAAO,GAAG,GAAG,SAAS,QAAQ,OAAO,CAAC;AAC/E,eAAS;AAAA,QACP;AAAA,QACA,uBAAuB,QAAQ,IAAI;AAAA,MACrC;AAAA,IACF,OAAO;AACL,aAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,SAAS,QAAQ,QAAQ,CAAC;AAAA,IAC9D;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,SAAoE;AACpF,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,UAAU,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,IAAI,CAAC;AACxD;AAYO,SAAS,cAAc,cAAqC,UAA0B,CAAC,GAAoB;AAChH,OAAK;AACL,QAAM,MAAuB,CAAC;AAC9B,MAAI,aAAa,QAAQ;AACvB,QAAI,KAAK,EAAE,MAAM,UAAU,SAAS,OAAO,aAAa,MAAM,EAAE,CAAC;AAAA,EACnE;AAEA,aAAW,WAAW,aAAa,UAAU;AAC3C,UAAM,SAAS,SAAS,QAAQ,OAAO;AAEvC,QAAI,QAAQ,SAAS,QAAQ;AAC3B,YAAM,cAAc,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,aAAa;AACjE,YAAM,gBAAgB,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,aAAa;AACnE,iBAAW,SAAS,aAAa;AAC/B,YAAI,KAAK;AAAA,UACP,MAAM;AAAA,UACN,cAAc,OAAQ,MAAmC,eAAe,EAAE;AAAA,UAC1E,SAAS,OAAQ,MAAgC,OAAO;AAAA,QAC1D,CAAC;AAAA,MACH;AACA,UAAI,cAAc,SAAS,GAAG;AAC5B,YAAI,KAAK,EAAE,MAAM,QAAQ,SAAS,oBAAoB,aAAa,EAAE,CAAC;AAAA,MACxE;AACA;AAAA,IACF;AAGA,UAAM,OAAO,OAAO,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,CAAC;AAC3D,UAAM,WAAW,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,UAAU;AAC3D,UAAM,YAAoC,EAAE,MAAM,aAAa,SAAS,QAAQ,KAAK;AACrF,QAAI,SAAS,SAAS,GAAG;AACvB,gBAAU,aAAa,SAAS,IAAI,CAAC,UAAU;AAC7C,cAAM,IAAI;AACV,eAAO,EAAE,IAAI,EAAE,IAAI,MAAM,YAAY,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,KAAK,UAAU,EAAE,SAAS,CAAC,CAAC,EAAE,EAAE;AAAA,MAC5G,CAAC;AAAA,IACH;AACA,QAAI,KAAK,SAAS;AAAA,EACpB;AAEA,SAAO;AACT;AAGA,SAAS,oBAAoB,QAA+D;AAC1F,QAAM,WAAW,OAAO,KAAK,CAAC,UAAU,mBAAmB,KAAK,MAAM,IAAI;AAC1E,MAAI,CAAC,SAAU,QAAO,OAAO,MAAM;AACnC,QAAM,QAA6B,CAAC;AACpC,aAAW,SAAS,QAAQ;AAC1B,UAAM,QAAQ,mBAAmB,KAAK;AACtC,QAAI,OAAO;AACT,YAAM,KAAK,cAAc,KAAK,CAAC;AAAA,IACjC,WAAW,SAAS,KAAK,KAAK,MAAM,SAAS,UAAU,OAAO,MAAM,SAAS,UAAU;AACrF,YAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,MAAM,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF;AACA,SAAO;AACT;;;AC3JO,SAAS,SAAS,UAA2B,UAA0B,CAAC,GAAuB;AACpG,QAAM,WAAW,IAAI,SAAS,OAAO;AACrC,QAAM,EAAE,QAAQ,KAAK,IAAI,YAAY,UAAU,QAAQ;AAEvD,QAAM,WAAW,oBAAI,IAAoB;AACzC,aAAW,WAAW,MAAM;AAC1B,QAAI,QAAQ,SAAS,eAAe,QAAQ,YAAY;AACtD,iBAAW,QAAQ,QAAQ,WAAY,UAAS,IAAI,KAAK,IAAI,KAAK,SAAS,IAAI;AAAA,IACjF;AAAA,EACF;AAEA,QAAM,WAA4B,CAAC;AACnC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,UAAU,KAAK,CAAC;AAEtB,QAAI,QAAQ,SAAS,QAAQ;AAC3B,YAAM,QAAsB,CAAC;AAC7B,UAAI,IAAI;AACR,aAAO,IAAI,KAAK,UAAU,KAAK,CAAC,EAAE,SAAS,QAAQ;AACjD,cAAM,OAAO,KAAK,CAAC;AACnB,cAAM,OAAO,SAAS,IAAI,KAAK,YAAY;AAC3C,YAAI,CAAC,MAAM;AACT,mBAAS;AAAA,YACP;AAAA,YACA,gBAAgB,KAAK,YAAY;AAAA,UACnC;AAAA,QACF;AACA,cAAM,KAAK;AAAA,UACT,kBAAkB;AAAA,YAChB,IAAI,KAAK;AAAA,YACT,MAAM,QAAQ,KAAK;AAAA,YACnB,UAAU,aAAa,OAAO,KAAK,OAAO,CAAC;AAAA,UAC7C;AAAA,QACF,CAAC;AACD;AAAA,MACF;AACA,eAAS,KAAK,EAAE,MAAM,QAAQ,MAAM,CAAC;AACrC,UAAI,IAAI;AACR;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,QAAQ;AAC3B,eAAS,KAAK,EAAE,MAAM,QAAQ,OAAO,UAAU,QAAQ,SAAS,QAAQ,EAAE,CAAC;AAC3E;AAAA,IACF;AAEA,aAAS,KAAK,EAAE,MAAM,SAAS,OAAO,eAAe,SAAS,QAAQ,EAAE,CAAC;AAAA,EAC3E;AAEA,QAAM,SAASA,kBAAiB,UAAU,QAAQ;AAClD,SAAO,WAAW,SACd,EAAE,UAAU,OAAO,IACnB,EAAE,mBAAmB,EAAE,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,EAAE,GAAG,UAAU,OAAO;AAC3E;AAEA,SAAS,UAAU,SAAuC,UAAkC;AAC1F,MAAI,OAAO,YAAY,SAAU,QAAO,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC1D,QAAM,QAAsB,CAAC;AAC7B,aAAW,QAAQ,SAAS;AAC1B,QAAI,SAAS,IAAI,KAAK,KAAK,SAAS,UAAU,OAAO,KAAK,SAAS,UAAU;AAC3E,YAAM,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC;AAC9B;AAAA,IACF;AACA,UAAM,QAAQ,gBAAgB,IAAI;AAClC,QAAI,OAAO;AACT,YAAM,KAAK,cAAc,OAAO,QAAQ,CAAC;AACzC;AAAA,IACF;AACA,aAAS,KAAK,mBAAmB,2CAA2C;AAAA,EAC9E;AACA,SAAO,MAAM,SAAS,IAAI,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC;AACjD;AAEA,SAAS,eAAe,SAAiC,UAAkC;AACzF,QAAM,QAAsB,CAAC;AAC7B,QAAM,OAAO,OAAO,QAAQ,WAAW,EAAE;AACzC,MAAI,KAAM,OAAM,KAAK,EAAE,KAAK,CAAC;AAC7B,aAAW,QAAQ,QAAQ,cAAc,CAAC,GAAG;AAC3C,UAAM,KAAK;AAAA,MACT,cAAc;AAAA,QACZ,IAAI,KAAK;AAAA,QACT,MAAM,KAAK,SAAS;AAAA,QACpB,MAAM,eAAe,KAAK,SAAS,WAAW,UAAU,KAAK,SAAS,IAAI;AAAA,MAC5E;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO,MAAM,SAAS,IAAI,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC;AACjD;AAGA,SAASA,kBAAiB,UAA2B,UAAqC;AACxF,QAAM,SAA0B,CAAC;AACjC,aAAW,WAAW,UAAU;AAC9B,UAAM,WAAW,OAAO,OAAO,SAAS,CAAC;AACzC,QAAI,YAAY,SAAS,SAAS,QAAQ,MAAM;AAC9C,eAAS,QAAQ,CAAC,GAAG,SAAS,OAAO,GAAG,QAAQ,KAAK;AACrD,eAAS,KAAK,eAAe,uBAAuB,QAAQ,IAAI,8CAA8C;AAAA,IAChH,OAAO;AACL,aAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,OAAO,CAAC,GAAG,QAAQ,KAAK,EAAE,CAAC;AAAA,IAC/D;AAAA,EACF;AACA,SAAO;AACT;AAaO,SAAS,WAAW,cAAkC,UAA0B,CAAC,GAAoB;AAC1G,QAAM,WAAW,IAAI,SAAS,OAAO;AACrC,QAAM,MAAuB,CAAC;AAE9B,MAAI,aAAa,mBAAmB;AAClC,UAAM,OAAO,OAAO,aAAa,kBAAkB,KAAK;AACxD,QAAI,KAAM,KAAI,KAAK,EAAE,MAAM,UAAU,SAAS,KAAK,CAAC;AAAA,EACtD;AAEA,QAAM,UAA0C,CAAC;AACjD,MAAI,UAAU;AACd,QAAM,aAAa,CAAC,SAAyB,QAAQ,KAAK,QAAQ,mBAAmB,GAAG,CAAC,IAAI,SAAS;AAEtG,aAAW,WAAW,aAAa,UAAU;AAC3C,UAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAE9D,QAAI,QAAQ,SAAS,SAAS;AAC5B,YAAM,aAAuB,CAAC;AAC9B,YAAM,YAA8B,CAAC;AACrC,iBAAW,QAAQ,OAAO;AACxB,YAAI,SAAS,IAAI,KAAK,SAAS,KAAK,YAAY,GAAG;AACjD,gBAAM,KAAK,KAAK;AAChB,gBAAM,KAAK,GAAG,MAAM,WAAW,GAAG,IAAI;AACtC,cAAI,CAAC,GAAG,GAAI,UAAS,KAAK,gBAAgB,wBAAwB,GAAG,IAAI,2BAA2B,EAAE,IAAI;AAC1G,oBAAU,KAAK;AAAA,YACb;AAAA,YACA,MAAM;AAAA,YACN,UAAU,EAAE,MAAM,GAAG,MAAM,WAAW,KAAK,UAAU,GAAG,QAAQ,CAAC,CAAC,EAAE;AAAA,UACtE,CAAC;AACD,kBAAQ,KAAK,EAAE,IAAI,MAAM,GAAG,KAAK,CAAC;AAAA,QACpC,WAAW,SAAS,IAAI,KAAK,OAAO,KAAK,SAAS,UAAU;AAC1D,qBAAW,KAAK,KAAK,IAAI;AAAA,QAC3B;AAAA,MACF;AACA,YAAM,OAAO,WAAW,KAAK,EAAE;AAC/B,YAAM,YAAoC,EAAE,MAAM,aAAa,SAAS,QAAQ,KAAK;AACrF,UAAI,UAAU,SAAS,EAAG,WAAU,aAAa;AACjD,UAAI,KAAK,SAAS;AAClB;AAAA,IACF;AAGA,UAAM,eAAoC,CAAC;AAC3C,QAAI,WAAW;AACf,eAAW,QAAQ,OAAO;AACxB,UAAI,SAAS,IAAI,KAAK,SAAS,KAAK,gBAAgB,GAAG;AACrD,cAAM,KAAK,KAAK;AAChB,cAAM,KAAK,kBAAkB,IAAI,SAAS,UAAU,UAAU;AAC9D,YAAI,KAAK,EAAE,MAAM,QAAQ,cAAc,IAAI,SAAS,eAAe,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC;AACvF;AAAA,MACF;AACA,YAAM,QAAQ,gBAAgB,IAAI;AAClC,UAAI,OAAO;AACT,qBAAa,KAAK,cAAc,KAAK,CAAC;AACtC,mBAAW;AACX;AAAA,MACF;AACA,UAAI,SAAS,IAAI,KAAK,OAAO,KAAK,SAAS,UAAU;AACnD,qBAAa,KAAK,EAAE,MAAM,QAAQ,MAAM,KAAK,KAAK,CAAC;AAAA,MACrD;AAAA,IACF;AACA,QAAI,aAAa,SAAS,GAAG;AAC3B,UAAI,UAAU;AACZ,YAAI,KAAK,EAAE,MAAM,QAAQ,SAAS,aAAa,CAAC;AAAA,MAClD,OAAO;AACL,cAAM,OAAO,OAAO,YAAY;AAChC,YAAI,KAAM,KAAI,KAAK,EAAE,MAAM,QAAQ,SAAS,KAAK,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBACP,UACA,SACA,UACA,YACQ;AACR,MAAI,SAAS,IAAI;AACf,UAAMC,SAAQ,QAAQ,UAAU,CAAC,MAAM,EAAE,OAAO,SAAS,EAAE;AAC3D,QAAIA,UAAS,EAAG,SAAQ,OAAOA,QAAO,CAAC;AACvC,WAAO,SAAS;AAAA,EAClB;AACA,QAAM,QAAQ,QAAQ,UAAU,CAAC,MAAM,EAAE,SAAS,SAAS,IAAI;AAC/D,MAAI,SAAS,GAAG;AACd,UAAM,EAAE,IAAAC,IAAG,IAAI,QAAQ,KAAK;AAC5B,YAAQ,OAAO,OAAO,CAAC;AACvB,WAAOA;AAAA,EACT;AACA,QAAM,KAAK,WAAW,SAAS,IAAI;AACnC,WAAS;AAAA,IACP;AAAA,IACA,gCAAgC,SAAS,IAAI,sCAAsC,EAAE;AAAA,EACvF;AACA,SAAO;AACT;;;AC3NO,SAAS,QACd,cACA,OACA,UAA0B,CAAC,GACP;AACpB,QAAM,YAAY,YAAY,cAAc,MAAM,MAAM,OAAO;AAC/D,SAAO,cAAc,WAAW,MAAM,IAAI,OAAO;AACnD;AAEA,SAAS,YAAY,cAAuB,MAAgB,SAA0C;AACpG,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,cAAc,cAAuC,OAAO;AAAA,IACrE,KAAK;AACH,aAAO,WAAW,cAAoC,OAAO;AAAA,IAC/D;AACE,YAAM,IAAI,MAAM,4BAA4B,OAAO,IAAI,CAAC,EAAE;AAAA,EAC9D;AACF;AAEA,SAAS,cAAc,WAA4B,IAAc,SAAmD;AAClH,UAAQ,IAAI;AAAA,IACV,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,YAAY,WAAW,OAAO;AAAA,IACvC,KAAK;AACH,aAAO,SAAS,WAAW,OAAO;AAAA,IACpC;AACE,YAAM,IAAI,MAAM,4BAA4B,OAAO,EAAE,CAAC,EAAE;AAAA,EAC5D;AACF;","names":["mergeConsecutive","index","id"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "llm-messages",
3
- "version": "0.1.0",
4
- "description": "Convert chat conversations between OpenAI, Anthropic and Gemini message formats. Tool calls, system prompts and roles handled. Zero dependencies.",
3
+ "version": "0.2.0",
4
+ "description": "Convert chat conversations between OpenAI, Anthropic and Gemini message formats. Tool calls, images, system prompts and roles handled. Zero dependencies.",
5
5
  "keywords": [
6
6
  "openai",
7
7
  "anthropic",
@@ -63,13 +63,13 @@
63
63
  "prepare": "npm run build"
64
64
  },
65
65
  "devDependencies": {
66
- "@eslint/js": "^9.17.0",
67
- "@types/node": "^22.10.2",
68
- "eslint": "^9.17.0",
66
+ "@eslint/js": "^10.0.1",
67
+ "@types/node": "^25.9.1",
68
+ "eslint": "^10.4.1",
69
69
  "prettier": "^3.4.2",
70
70
  "tsup": "^8.3.5",
71
71
  "typescript": "^5.7.2",
72
- "typescript-eslint": "^8.18.1",
72
+ "typescript-eslint": "^8.60.0",
73
73
  "vitest": "^2.1.8"
74
74
  }
75
75
  }