llm-messages 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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' | 'gemini-url-image';
13
+ type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream' | 'gemini-url-image' | 'gemini-url-media' | 'unsupported-modality';
14
14
  /** A non fatal event raised during conversion. */
15
15
  interface Warning {
16
16
  code: WarningCode;
@@ -33,8 +33,25 @@ interface OpenAIImagePart {
33
33
  detail?: 'auto' | 'low' | 'high' | 'original';
34
34
  };
35
35
  }
36
- /** A content part. Unknown part types (audio, files) are preserved verbatim. */
37
- type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | {
36
+ interface OpenAIAudioPart {
37
+ type: 'input_audio';
38
+ /** `data` is raw base64 (no data URL prefix); `format` is `wav` or `mp3`. */
39
+ input_audio: {
40
+ data: string;
41
+ format: string;
42
+ };
43
+ }
44
+ interface OpenAIFilePart {
45
+ type: 'file';
46
+ /** `file_data` is a `data:<mediaType>;base64,<data>` data URL, or use `file_id`. */
47
+ file: {
48
+ file_data?: string;
49
+ file_id?: string;
50
+ filename?: string;
51
+ };
52
+ }
53
+ /** A content part. Unknown part types are preserved verbatim. */
54
+ type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | OpenAIAudioPart | OpenAIFilePart | {
38
55
  type: string;
39
56
  [key: string]: unknown;
40
57
  };
@@ -99,7 +116,21 @@ interface AnthropicImageBlock {
99
116
  url: string;
100
117
  };
101
118
  }
102
- type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | {
119
+ interface AnthropicDocumentBlock {
120
+ type: 'document';
121
+ source: {
122
+ type: 'base64';
123
+ media_type: string;
124
+ data: string;
125
+ } | {
126
+ type: 'url';
127
+ url: string;
128
+ } | {
129
+ type: 'file';
130
+ file_id: string;
131
+ };
132
+ }
133
+ type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | AnthropicDocumentBlock | {
103
134
  type: string;
104
135
  [key: string]: unknown;
105
136
  };
@@ -228,6 +259,28 @@ declare function parseDataUrl(url: string): {
228
259
  /** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */
229
260
  declare function toDataUrl(mediaType: string, data: string): string;
230
261
 
262
+ /**
263
+ * A provider-neutral non-image media part (audio or document). Images keep their
264
+ * own dedicated path in `image.ts`; everything else flows through here.
265
+ */
266
+ type MediaModality = 'audio' | 'document';
267
+ type MediaSource = {
268
+ kind: 'base64';
269
+ mediaType: string;
270
+ data: string;
271
+ } | {
272
+ kind: 'url';
273
+ url: string;
274
+ } | {
275
+ kind: 'file_id';
276
+ id: string;
277
+ };
278
+ interface MediaPart {
279
+ modality: MediaModality;
280
+ source: MediaSource;
281
+ filename?: string;
282
+ }
283
+
231
284
  /** A provider-neutral finish reason. */
232
285
  type FinishReason = 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'unknown';
233
286
  /** Provider-neutral token usage. */
@@ -252,4 +305,4 @@ declare function normalizeResponse(body: unknown, route: {
252
305
  from: Provider;
253
306
  }, options?: ConvertOptions): NormalizedResponse;
254
307
 
255
- export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
308
+ export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicDocumentBlock, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type MediaModality, type MediaPart, type MediaSource, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIAudioPart, type OpenAIContentPart, type OpenAIFilePart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  /** A supported provider. */
11
11
  type Provider = 'openai' | 'anthropic' | 'gemini';
12
12
  /** A stable, machine readable code describing a non fatal conversion event. */
13
- type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream' | 'gemini-url-image';
13
+ type WarningCode = 'generated-id' | 'unmapped-tool-result' | 'merged-role' | 'dropped-content' | 'invalid-json-arguments' | 'system-midstream' | 'gemini-url-image' | 'gemini-url-media' | 'unsupported-modality';
14
14
  /** A non fatal event raised during conversion. */
15
15
  interface Warning {
16
16
  code: WarningCode;
@@ -33,8 +33,25 @@ interface OpenAIImagePart {
33
33
  detail?: 'auto' | 'low' | 'high' | 'original';
34
34
  };
35
35
  }
36
- /** A content part. Unknown part types (audio, files) are preserved verbatim. */
37
- type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | {
36
+ interface OpenAIAudioPart {
37
+ type: 'input_audio';
38
+ /** `data` is raw base64 (no data URL prefix); `format` is `wav` or `mp3`. */
39
+ input_audio: {
40
+ data: string;
41
+ format: string;
42
+ };
43
+ }
44
+ interface OpenAIFilePart {
45
+ type: 'file';
46
+ /** `file_data` is a `data:<mediaType>;base64,<data>` data URL, or use `file_id`. */
47
+ file: {
48
+ file_data?: string;
49
+ file_id?: string;
50
+ filename?: string;
51
+ };
52
+ }
53
+ /** A content part. Unknown part types are preserved verbatim. */
54
+ type OpenAIContentPart = OpenAITextPart | OpenAIImagePart | OpenAIAudioPart | OpenAIFilePart | {
38
55
  type: string;
39
56
  [key: string]: unknown;
40
57
  };
@@ -99,7 +116,21 @@ interface AnthropicImageBlock {
99
116
  url: string;
100
117
  };
101
118
  }
102
- type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | {
119
+ interface AnthropicDocumentBlock {
120
+ type: 'document';
121
+ source: {
122
+ type: 'base64';
123
+ media_type: string;
124
+ data: string;
125
+ } | {
126
+ type: 'url';
127
+ url: string;
128
+ } | {
129
+ type: 'file';
130
+ file_id: string;
131
+ };
132
+ }
133
+ type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock | AnthropicToolResultBlock | AnthropicImageBlock | AnthropicDocumentBlock | {
103
134
  type: string;
104
135
  [key: string]: unknown;
105
136
  };
@@ -228,6 +259,28 @@ declare function parseDataUrl(url: string): {
228
259
  /** Reassembles a base64 data URL. The inverse of {@link parseDataUrl}. */
229
260
  declare function toDataUrl(mediaType: string, data: string): string;
230
261
 
262
+ /**
263
+ * A provider-neutral non-image media part (audio or document). Images keep their
264
+ * own dedicated path in `image.ts`; everything else flows through here.
265
+ */
266
+ type MediaModality = 'audio' | 'document';
267
+ type MediaSource = {
268
+ kind: 'base64';
269
+ mediaType: string;
270
+ data: string;
271
+ } | {
272
+ kind: 'url';
273
+ url: string;
274
+ } | {
275
+ kind: 'file_id';
276
+ id: string;
277
+ };
278
+ interface MediaPart {
279
+ modality: MediaModality;
280
+ source: MediaSource;
281
+ filename?: string;
282
+ }
283
+
231
284
  /** A provider-neutral finish reason. */
232
285
  type FinishReason = 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'unknown';
233
286
  /** Provider-neutral token usage. */
@@ -252,4 +305,4 @@ declare function normalizeResponse(body: unknown, route: {
252
305
  from: Provider;
253
306
  }, options?: ConvertOptions): NormalizedResponse;
254
307
 
255
- export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIContentPart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
308
+ export { type AnthropicContentBlock, type AnthropicConversation, type AnthropicDocumentBlock, type AnthropicImageBlock, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResultBlock, type AnthropicToolUseBlock, type ConversationOf, type ConvertOptions, type FinishReason, type GeminiContent, type GeminiConversation, type GeminiFileDataPart, type GeminiFunctionCallPart, type GeminiFunctionResponsePart, type GeminiInlineDataPart, type GeminiPart, type GeminiTextPart, type MediaModality, type MediaPart, type MediaSource, type NormalizedImage, type NormalizedResponse, type OpenAIAssistantMessage, type OpenAIAudioPart, type OpenAIContentPart, type OpenAIFilePart, type OpenAIImagePart, type OpenAIMessage, type OpenAISystemMessage, type OpenAITextPart, type OpenAIToolCall, type OpenAIToolMessage, type OpenAIUserMessage, type Provider, type Usage, type Warning, type WarningCode, convert, fromAnthropic, fromGemini, normalizeResponse, parseDataUrl, responseFromAnthropic, responseFromGemini, responseFromOpenAI, toAnthropic, toDataUrl, toGemini };
package/dist/index.js CHANGED
@@ -92,13 +92,16 @@ function imageToAnthropic(image) {
92
92
  function imageFromGemini(part) {
93
93
  if (isRecord(part) && isRecord(part.inlineData)) {
94
94
  const data = part.inlineData;
95
- if (typeof data.mimeType === "string" && typeof data.data === "string") {
95
+ if (typeof data.mimeType === "string" && typeof data.data === "string" && data.mimeType.startsWith("image/")) {
96
96
  return { kind: "base64", mediaType: data.mimeType, data: data.data };
97
97
  }
98
98
  }
99
99
  if (isRecord(part) && isRecord(part.fileData)) {
100
100
  const data = part.fileData;
101
- if (typeof data.fileUri === "string") return { kind: "url", url: data.fileUri };
101
+ const mime = typeof data.mimeType === "string" ? data.mimeType : "";
102
+ if (typeof data.fileUri === "string" && (mime === "" || mime.startsWith("image/"))) {
103
+ return { kind: "url", url: data.fileUri };
104
+ }
102
105
  }
103
106
  return null;
104
107
  }
@@ -113,6 +116,124 @@ function imageToGemini(image, reporter) {
113
116
  return { fileData: { fileUri: image.url } };
114
117
  }
115
118
 
119
+ // src/media.ts
120
+ function modalityFromMime(mediaType) {
121
+ return mediaType.startsWith("audio/") ? "audio" : "document";
122
+ }
123
+ function mediaFromOpenAI(part) {
124
+ if (!isRecord(part)) return null;
125
+ if (part.type === "input_audio" && isRecord(part.input_audio)) {
126
+ const audio = part.input_audio;
127
+ if (typeof audio.data === "string") {
128
+ const format = typeof audio.format === "string" ? audio.format : "wav";
129
+ return { modality: "audio", source: { kind: "base64", mediaType: `audio/${format}`, data: audio.data } };
130
+ }
131
+ }
132
+ if (part.type === "file" && isRecord(part.file)) {
133
+ const file = part.file;
134
+ const filename = typeof file.filename === "string" ? file.filename : void 0;
135
+ if (typeof file.file_data === "string") {
136
+ const parsed = parseDataUrl(file.file_data);
137
+ if (parsed) return { modality: "document", source: { kind: "base64", ...parsed }, filename };
138
+ }
139
+ if (typeof file.file_id === "string") {
140
+ return { modality: "document", source: { kind: "file_id", id: file.file_id }, filename };
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+ function mediaToOpenAI(media) {
146
+ const { modality, source } = media;
147
+ if (modality === "audio") {
148
+ if (source.kind !== "base64") return null;
149
+ const audio = {
150
+ type: "input_audio",
151
+ input_audio: { data: source.data, format: source.mediaType.replace(/^audio\//, "") }
152
+ };
153
+ return audio;
154
+ }
155
+ if (source.kind === "base64") {
156
+ const file = {
157
+ type: "file",
158
+ file: {
159
+ file_data: toDataUrl(source.mediaType, source.data),
160
+ ...media.filename ? { filename: media.filename } : {}
161
+ }
162
+ };
163
+ return file;
164
+ }
165
+ if (source.kind === "file_id") {
166
+ const file = {
167
+ type: "file",
168
+ file: { file_id: source.id, ...media.filename ? { filename: media.filename } : {} }
169
+ };
170
+ return file;
171
+ }
172
+ return null;
173
+ }
174
+ function mediaFromAnthropic(block) {
175
+ if (!isRecord(block) || block.type !== "document" || !isRecord(block.source)) return null;
176
+ const source = block.source;
177
+ if (source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string") {
178
+ return { modality: "document", source: { kind: "base64", mediaType: source.media_type, data: source.data } };
179
+ }
180
+ if (source.type === "url" && typeof source.url === "string") {
181
+ return { modality: "document", source: { kind: "url", url: source.url } };
182
+ }
183
+ if (source.type === "file" && typeof source.file_id === "string") {
184
+ return { modality: "document", source: { kind: "file_id", id: source.file_id } };
185
+ }
186
+ return null;
187
+ }
188
+ function mediaToAnthropic(media, reporter) {
189
+ if (media.modality === "audio") {
190
+ reporter.warn("unsupported-modality", "Anthropic has no audio input; dropped an audio part.");
191
+ return null;
192
+ }
193
+ const { source } = media;
194
+ if (source.kind === "base64") {
195
+ return { type: "document", source: { type: "base64", media_type: source.mediaType, data: source.data } };
196
+ }
197
+ if (source.kind === "url") {
198
+ return { type: "document", source: { type: "url", url: source.url } };
199
+ }
200
+ return { type: "document", source: { type: "file", file_id: source.id } };
201
+ }
202
+ function mediaFromGemini(part) {
203
+ if (isRecord(part) && isRecord(part.inlineData)) {
204
+ const data = part.inlineData;
205
+ if (typeof data.mimeType === "string" && typeof data.data === "string" && !data.mimeType.startsWith("image/")) {
206
+ return {
207
+ modality: modalityFromMime(data.mimeType),
208
+ source: { kind: "base64", mediaType: data.mimeType, data: data.data }
209
+ };
210
+ }
211
+ }
212
+ if (isRecord(part) && isRecord(part.fileData)) {
213
+ const data = part.fileData;
214
+ const mime = typeof data.mimeType === "string" ? data.mimeType : "";
215
+ if (typeof data.fileUri === "string" && mime !== "" && !mime.startsWith("image/")) {
216
+ return { modality: modalityFromMime(mime), source: { kind: "url", url: data.fileUri } };
217
+ }
218
+ }
219
+ return null;
220
+ }
221
+ function mediaToGemini(media, reporter) {
222
+ const { source } = media;
223
+ if (source.kind === "base64") {
224
+ return { inlineData: { mimeType: source.mediaType, data: source.data } };
225
+ }
226
+ if (source.kind === "url") {
227
+ reporter.warn(
228
+ "gemini-url-media",
229
+ "A media URL was emitted as Gemini fileData.fileUri; Gemini may require the Files API for non-Google URIs."
230
+ );
231
+ return { fileData: { fileUri: source.url } };
232
+ }
233
+ reporter.warn("unsupported-modality", "Gemini has no file-id media reference; dropped a file_id part.");
234
+ return null;
235
+ }
236
+
116
237
  // src/providers/openai.ts
117
238
  function isSystem(message) {
118
239
  return message.role === "system" || message.role === "developer";
@@ -181,6 +302,12 @@ function userContent(content, reporter) {
181
302
  blocks.push(imageToAnthropic(image));
182
303
  continue;
183
304
  }
305
+ const media = mediaFromOpenAI(part);
306
+ if (media) {
307
+ const block = mediaToAnthropic(media, reporter);
308
+ if (block) blocks.push(block);
309
+ continue;
310
+ }
184
311
  reporter.warn("dropped-content", "Dropped an unsupported user content part.");
185
312
  }
186
313
  return blocks;
@@ -222,7 +349,7 @@ function asBlocks(content) {
222
349
  return content ? [{ type: "text", text: content }] : [];
223
350
  }
224
351
  function fromAnthropic(conversation, options = {}) {
225
- void options;
352
+ const reporter = new Reporter(options);
226
353
  const out = [];
227
354
  if (conversation.system) {
228
355
  out.push({ role: "system", content: textOf(conversation.system) });
@@ -240,7 +367,7 @@ function fromAnthropic(conversation, options = {}) {
240
367
  });
241
368
  }
242
369
  if (contentBlocks.length > 0) {
243
- out.push({ role: "user", content: userContentToOpenAI(contentBlocks) });
370
+ out.push({ role: "user", content: userContentToOpenAI(contentBlocks, reporter) });
244
371
  }
245
372
  continue;
246
373
  }
@@ -257,15 +384,24 @@ function fromAnthropic(conversation, options = {}) {
257
384
  }
258
385
  return out;
259
386
  }
260
- function userContentToOpenAI(blocks) {
261
- const hasImage = blocks.some((block) => imageFromAnthropic(block) !== null);
262
- if (!hasImage) return textOf(blocks);
387
+ function userContentToOpenAI(blocks, reporter) {
388
+ const hasMedia = blocks.some((block) => imageFromAnthropic(block) !== null || mediaFromAnthropic(block) !== null);
389
+ if (!hasMedia) return textOf(blocks);
263
390
  const parts = [];
264
391
  for (const block of blocks) {
265
392
  const image = imageFromAnthropic(block);
266
393
  if (image) {
267
394
  parts.push(imageToOpenAI(image));
268
- } else if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
395
+ continue;
396
+ }
397
+ const media = mediaFromAnthropic(block);
398
+ if (media) {
399
+ const part = mediaToOpenAI(media);
400
+ if (part) parts.push(part);
401
+ else reporter.warn("dropped-content", "A document URL has no OpenAI Chat Completions equivalent; dropped.");
402
+ continue;
403
+ }
404
+ if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
269
405
  parts.push({ type: "text", text: block.text });
270
406
  }
271
407
  }
@@ -332,6 +468,12 @@ function userParts(content, reporter) {
332
468
  parts.push(imageToGemini(image, reporter));
333
469
  continue;
334
470
  }
471
+ const media = mediaFromOpenAI(part);
472
+ if (media) {
473
+ const geminiPart = mediaToGemini(media, reporter);
474
+ if (geminiPart) parts.push(geminiPart);
475
+ continue;
476
+ }
335
477
  reporter.warn("dropped-content", "Dropped an unsupported user content part.");
336
478
  }
337
479
  return parts.length > 0 ? parts : [{ text: "" }];
@@ -401,7 +543,7 @@ function fromGemini(conversation, options = {}) {
401
543
  continue;
402
544
  }
403
545
  const contentParts = [];
404
- let hasImage = false;
546
+ let hasMedia = false;
405
547
  for (const part of parts) {
406
548
  if (isRecord(part) && isRecord(part.functionResponse)) {
407
549
  const fr = part.functionResponse;
@@ -412,7 +554,16 @@ function fromGemini(conversation, options = {}) {
412
554
  const image = imageFromGemini(part);
413
555
  if (image) {
414
556
  contentParts.push(imageToOpenAI(image));
415
- hasImage = true;
557
+ hasMedia = true;
558
+ continue;
559
+ }
560
+ const media = mediaFromGemini(part);
561
+ if (media) {
562
+ const openaiPart = mediaToOpenAI(media);
563
+ if (openaiPart) {
564
+ contentParts.push(openaiPart);
565
+ hasMedia = true;
566
+ }
416
567
  continue;
417
568
  }
418
569
  if (isRecord(part) && typeof part.text === "string") {
@@ -420,7 +571,7 @@ function fromGemini(conversation, options = {}) {
420
571
  }
421
572
  }
422
573
  if (contentParts.length > 0) {
423
- if (hasImage) {
574
+ if (hasMedia) {
424
575
  out.push({ role: "user", content: contentParts });
425
576
  } else {
426
577
  const text = textOf(contentParts);