llm-messages 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,7 +31,8 @@ __export(index_exports, {
31
31
  responseFromOpenAIResponses: () => responseFromOpenAIResponses,
32
32
  toAnthropic: () => toAnthropic,
33
33
  toDataUrl: () => toDataUrl,
34
- toGemini: () => toGemini
34
+ toGemini: () => toGemini,
35
+ warningCodes: () => warningCodes
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
 
@@ -54,6 +55,7 @@ function textOf(content) {
54
55
  return content.map((part) => {
55
56
  if (typeof part === "string") return part;
56
57
  if (isRecord(part) && typeof part.text === "string") return part.text;
58
+ if (isRecord(part) && part.type === "refusal" && typeof part.refusal === "string") return part.refusal;
57
59
  return "";
58
60
  }).join("");
59
61
  }
@@ -75,17 +77,83 @@ function parseArguments(args, reporter, fnName) {
75
77
  );
76
78
  return {};
77
79
  }
80
+ function stringifyArgumentsObject(value, reporter, provider, part, fnName) {
81
+ if (value === void 0) return "{}";
82
+ if (isRecord(value)) {
83
+ try {
84
+ return JSON.stringify(value);
85
+ } catch {
86
+ reporter.warn(
87
+ "invalid-json-arguments",
88
+ `${provider} ${part} '${fnName}' had arguments that could not be serialized as JSON; used an empty object instead.`
89
+ );
90
+ return "{}";
91
+ }
92
+ }
93
+ reporter.warn(
94
+ "invalid-json-arguments",
95
+ `${provider} ${part} '${fnName}' had arguments that were not an object; used an empty object instead.`
96
+ );
97
+ return "{}";
98
+ }
99
+ var OPENAI_FUNCTION_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
100
+ function isProviderFunctionName(value) {
101
+ return typeof value === "string" && OPENAI_FUNCTION_NAME.test(value);
102
+ }
103
+ function providerFunctionName(value, reporter, provider, part) {
104
+ if (isProviderFunctionName(value)) return value;
105
+ reporter.warn(
106
+ "dropped-metadata",
107
+ `${provider} ${part} had a missing or invalid function name; used 'unknown_function'.`
108
+ );
109
+ return "unknown_function";
110
+ }
111
+ function createToolCallIdGenerator(reservedIds = []) {
112
+ const reserved = new Set(reservedIds);
113
+ const used = /* @__PURE__ */ new Set();
114
+ let counter = 0;
115
+ const generate = (name) => {
116
+ let id;
117
+ do {
118
+ id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
119
+ } while (used.has(id) || reserved.has(id));
120
+ used.add(id);
121
+ return id;
122
+ };
123
+ return {
124
+ claim(id, name) {
125
+ if (used.has(id)) return generate(name);
126
+ used.add(id);
127
+ reserved.delete(id);
128
+ return id;
129
+ },
130
+ generate(name) {
131
+ return generate(name);
132
+ }
133
+ };
134
+ }
78
135
  function wrapResponse(content) {
79
136
  const parsed = tryParseJson(content);
80
137
  if (parsed.ok && isRecord(parsed.value)) return parsed.value;
81
138
  return { result: content };
82
139
  }
83
- function unwrapResponse(response) {
84
- const keys = Object.keys(response);
85
- if (keys.length === 1 && keys[0] === "result" && typeof response.result === "string") {
86
- return response.result;
140
+ function unwrapResponse(response, reporter, provider = "Provider", part = "response") {
141
+ if (response === void 0) return "{}";
142
+ if (isRecord(response)) {
143
+ const keys = Object.keys(response);
144
+ if (keys.length === 1 && keys[0] === "result" && typeof response.result === "string") {
145
+ return response.result;
146
+ }
147
+ }
148
+ try {
149
+ return JSON.stringify(response) ?? "{}";
150
+ } catch {
151
+ reporter?.warn(
152
+ "dropped-content",
153
+ `${provider} ${part} response could not be serialized as JSON; used an empty object instead.`
154
+ );
155
+ return "{}";
87
156
  }
88
- return JSON.stringify(response);
89
157
  }
90
158
 
91
159
  // src/image.ts
@@ -304,6 +372,7 @@ function splitSystem(messages, reporter) {
304
372
  }
305
373
 
306
374
  // src/providers/anthropic.ts
375
+ var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
307
376
  function toAnthropic(messages, options = {}) {
308
377
  const reporter = new Reporter(options);
309
378
  const { system, rest } = splitSystem(messages, reporter);
@@ -365,7 +434,7 @@ function userContent(content, reporter) {
365
434
  }
366
435
  reporter.warn("dropped-content", "Dropped an unsupported user content part.");
367
436
  }
368
- return blocks;
437
+ return blocks.length > 0 ? blocks : "";
369
438
  }
370
439
  function assistantContent(message, reporter) {
371
440
  warnDroppedName("Assistant", message.name, "Anthropic", reporter);
@@ -405,17 +474,59 @@ function mergeConsecutive(messages, reporter) {
405
474
  return result;
406
475
  }
407
476
  function asBlocks(content) {
408
- if (typeof content !== "string") return content;
477
+ if (Array.isArray(content)) return content.filter(isRecord);
478
+ if (typeof content !== "string") return [];
409
479
  return content ? [{ type: "text", text: content }] : [];
410
480
  }
481
+ function warnMalformedBlocks(content, reporter) {
482
+ if (Array.isArray(content)) {
483
+ for (const block of content) {
484
+ if (!isRecord(block)) {
485
+ reporter.warn("dropped-content", "Dropped a malformed Anthropic content block.");
486
+ }
487
+ }
488
+ return;
489
+ }
490
+ if (typeof content !== "string") {
491
+ reporter.warn("dropped-content", "Dropped malformed Anthropic message content.");
492
+ }
493
+ }
494
+ function isSupportedUserBlock(block) {
495
+ return block.type === "text" && typeof block.text === "string" || block.type === "tool_result" || imageFromAnthropic(block) !== null || mediaFromAnthropic(block) !== null;
496
+ }
497
+ function isSupportedAssistantBlock(block) {
498
+ return block.type === "text" && typeof block.text === "string" || block.type === "tool_use";
499
+ }
500
+ function warnUnsupportedBlocks(blocks, role, reporter) {
501
+ const isSupported = role === "user" ? isSupportedUserBlock : isSupportedAssistantBlock;
502
+ for (const block of blocks) {
503
+ if (isSupported(block)) continue;
504
+ reporter.warn("dropped-content", `Dropped unsupported Anthropic ${role} content block '${String(block.type)}'.`);
505
+ }
506
+ }
411
507
  function fromAnthropic(conversation, options = {}) {
412
508
  const reporter = new Reporter(options);
413
509
  const out = [];
414
- if (conversation.system) {
415
- out.push({ role: "system", content: textOf(conversation.system) });
510
+ const root = isRecord(conversation) ? conversation : {};
511
+ const system = textOf(root.system);
512
+ if (system) {
513
+ out.push({ role: "system", content: system });
416
514
  }
417
- for (const message of conversation.messages) {
515
+ const messages = Array.isArray(root.messages) ? root.messages : [];
516
+ const ids = createToolCallIdGenerator(anthropicProviderIds(root));
517
+ const pendingToolUseIds = /* @__PURE__ */ new Map();
518
+ for (const message of messages) {
519
+ if (!isRecord(message)) {
520
+ reporter.warn("dropped-content", "Dropped a malformed Anthropic message.");
521
+ continue;
522
+ }
523
+ if (message.role !== "user" && message.role !== "assistant") {
524
+ reporter.warn("dropped-content", `Dropped an Anthropic message with unsupported role '${String(message.role)}'.`);
525
+ continue;
526
+ }
527
+ warnMalformedBlocks(message.content, reporter);
418
528
  const blocks = asBlocks(message.content);
529
+ warnUnsupportedBlocks(blocks, message.role, reporter);
419
530
  if (message.role === "user") {
420
531
  if (blocks.length === 0) {
421
532
  out.push({ role: "user", content: "" });
@@ -434,9 +545,28 @@ function fromAnthropic(conversation, options = {}) {
434
545
  continue;
435
546
  }
436
547
  flushContent();
548
+ const rawToolUseId = block.tool_use_id;
549
+ const hasToolUseId = nonEmptyString(rawToolUseId);
550
+ const matchedToolCallId = hasToolUseId ? takePendingToolUseId(pendingToolUseIds, rawToolUseId) : void 0;
551
+ const toolCallId = matchedToolCallId ?? (hasToolUseId ? ids.claim(rawToolUseId, "tool_result") : ids.generate("tool_result"));
552
+ if (!hasToolUseId) {
553
+ reporter.warn(
554
+ "unmapped-tool-result",
555
+ `Anthropic tool_result had no usable tool_use_id; generated '${toolCallId}'.`
556
+ );
557
+ } else if (!matchedToolCallId) {
558
+ const unmappedMessage = toolCallId === rawToolUseId ? `Anthropic tool_result '${rawToolUseId}' had no matching tool_use; kept the result id.` : `Anthropic tool_result '${rawToolUseId}' had no matching tool_use; generated '${toolCallId}'.`;
559
+ reporter.warn("unmapped-tool-result", unmappedMessage);
560
+ if (toolCallId !== rawToolUseId) {
561
+ reporter.warn(
562
+ "generated-id",
563
+ `Anthropic tool_result '${rawToolUseId}' reused an existing id; generated '${toolCallId}'.`
564
+ );
565
+ }
566
+ }
437
567
  out.push({
438
568
  role: "tool",
439
- tool_call_id: String(block.tool_use_id ?? ""),
569
+ tool_call_id: toolCallId,
440
570
  content: textOf(block.content),
441
571
  ...typeof block.is_error === "boolean" ? { is_error: block.is_error } : {}
442
572
  });
@@ -450,13 +580,57 @@ function fromAnthropic(conversation, options = {}) {
450
580
  if (toolUses.length > 0) {
451
581
  assistant.tool_calls = toolUses.map((block) => {
452
582
  const b = block;
453
- return { id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };
583
+ const name = providerFunctionName(b.name, reporter, "Anthropic", "tool_use");
584
+ const providedId = nonEmptyString(b.id) ? b.id : void 0;
585
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
586
+ if (!providedId) {
587
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' had no id; generated '${id}'.`);
588
+ } else if (id !== providedId) {
589
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' reused id '${providedId}'; generated '${id}'.`);
590
+ }
591
+ if (providedId) pushPendingToolUseId(pendingToolUseIds, providedId, id);
592
+ return {
593
+ id,
594
+ type: "function",
595
+ function: {
596
+ name,
597
+ arguments: stringifyArgumentsObject(b.input, reporter, "Anthropic", "tool_use", name)
598
+ }
599
+ };
454
600
  });
455
601
  }
456
602
  out.push(assistant);
457
603
  }
458
604
  return out;
459
605
  }
606
+ function pushPendingToolUseId(pending, providerId, normalizedId) {
607
+ const ids = pending.get(providerId);
608
+ if (ids) ids.push(normalizedId);
609
+ else pending.set(providerId, [normalizedId]);
610
+ }
611
+ function takePendingToolUseId(pending, providerId) {
612
+ const ids = pending.get(providerId);
613
+ const id = ids?.shift();
614
+ if (ids?.length === 0) pending.delete(providerId);
615
+ return id;
616
+ }
617
+ function anthropicProviderIds(conversation) {
618
+ const ids = [];
619
+ const root = isRecord(conversation) ? conversation : {};
620
+ const messages = Array.isArray(root.messages) ? root.messages : [];
621
+ for (const message of messages) {
622
+ if (!isRecord(message)) continue;
623
+ for (const block of asBlocks(message.content)) {
624
+ if (block.type === "tool_use" && nonEmptyString(block.id)) {
625
+ ids.push(block.id);
626
+ }
627
+ if (block.type === "tool_result" && nonEmptyString(block.tool_use_id)) {
628
+ ids.push(block.tool_use_id);
629
+ }
630
+ }
631
+ }
632
+ return ids;
633
+ }
460
634
  function userContentToOpenAI(blocks, reporter) {
461
635
  const hasMedia = blocks.some((block) => imageFromAnthropic(block) !== null || mediaFromAnthropic(block) !== null);
462
636
  if (!hasMedia) return textOf(blocks);
@@ -478,10 +652,11 @@ function userContentToOpenAI(blocks, reporter) {
478
652
  parts.push({ type: "text", text: block.text });
479
653
  }
480
654
  }
481
- return parts;
655
+ return parts.length > 0 ? parts : "";
482
656
  }
483
657
 
484
658
  // src/providers/gemini.ts
659
+ var nonEmptyString2 = (value) => typeof value === "string" && value.length > 0;
485
660
  function toGemini(messages, options = {}) {
486
661
  const reporter = new Reporter(options);
487
662
  const { system, rest } = splitSystem(messages, reporter);
@@ -512,8 +687,9 @@ function toGemini(messages, options = {}) {
512
687
  `Tool message name '${tool.name}' differs from matching tool call '${matchingName}'; used the tool-call function name for Gemini.`
513
688
  );
514
689
  }
515
- const name = matchingName ?? tool.name;
516
- if (!name) {
690
+ const hasStandaloneName = nonEmptyString2(tool.name);
691
+ const name = matchingName ?? (hasStandaloneName ? tool.name : tool.tool_call_id);
692
+ if (!matchingName && !hasStandaloneName) {
517
693
  reporter.warn(
518
694
  "unmapped-tool-result",
519
695
  `Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`
@@ -601,31 +777,51 @@ function mergeConsecutive2(contents, reporter) {
601
777
  function fromGemini(conversation, options = {}) {
602
778
  const reporter = new Reporter(options);
603
779
  const out = [];
604
- if (conversation.systemInstruction) {
605
- const text = textOf(conversation.systemInstruction.parts);
780
+ const root = isRecord(conversation) ? conversation : {};
781
+ const systemInstruction = isRecord(root.systemInstruction) ? root.systemInstruction : void 0;
782
+ if (systemInstruction) {
783
+ const text = textOf(systemInstruction.parts);
606
784
  if (text) out.push({ role: "system", content: text });
607
785
  }
608
786
  const pending = [];
609
- let counter = 0;
610
- const generateId = (name) => `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
611
- for (const content of conversation.contents) {
612
- const parts = Array.isArray(content.parts) ? content.parts : [];
787
+ const contents = Array.isArray(root.contents) ? root.contents : [];
788
+ const ids = createToolCallIdGenerator(geminiProviderIds(root));
789
+ for (const content of contents) {
790
+ if (!isRecord(content)) {
791
+ reporter.warn("dropped-content", "Dropped a malformed Gemini content entry.");
792
+ continue;
793
+ }
613
794
  if (content.role === "model") {
795
+ const parts2 = geminiParts(content, reporter);
614
796
  const textPieces = [];
615
797
  const toolCalls = [];
616
- for (const part of parts) {
798
+ for (const part of parts2) {
617
799
  if (isRecord(part) && isRecord(part.functionCall)) {
618
800
  const fc = part.functionCall;
619
- const id = fc.id ?? generateId(fc.name);
620
- if (!fc.id) reporter.warn("generated-id", `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);
801
+ const name = providerFunctionName(fc.name, reporter, "Gemini", "functionCall");
802
+ const providedId = nonEmptyString2(fc.id) ? fc.id : void 0;
803
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
804
+ if (!providedId) {
805
+ reporter.warn("generated-id", `Gemini functionCall '${name}' had no id; generated '${id}'.`);
806
+ } else if (id !== providedId) {
807
+ reporter.warn(
808
+ "generated-id",
809
+ `Gemini functionCall '${name}' reused id '${providedId}'; generated '${id}'.`
810
+ );
811
+ }
621
812
  toolCalls.push({
622
813
  id,
623
814
  type: "function",
624
- function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) }
815
+ function: {
816
+ name,
817
+ arguments: stringifyArgumentsObject(fc.args, reporter, "Gemini", "functionCall", name)
818
+ }
625
819
  });
626
- pending.push({ id, name: fc.name });
820
+ pending.push({ id, name, ...providedId ? { providerId: providedId } : {} });
627
821
  } else if (isRecord(part) && typeof part.text === "string") {
628
822
  textPieces.push(part.text);
823
+ } else {
824
+ reporter.warn("dropped-content", "Dropped an unsupported Gemini model content part.");
629
825
  }
630
826
  }
631
827
  const text = textPieces.join("");
@@ -634,17 +830,45 @@ function fromGemini(conversation, options = {}) {
634
830
  out.push(assistant);
635
831
  continue;
636
832
  }
637
- const contentParts = [];
833
+ if (content.role !== void 0 && content.role !== "user") {
834
+ reporter.warn(
835
+ "dropped-content",
836
+ `Dropped a Gemini content entry with unsupported role '${String(content.role)}'.`
837
+ );
838
+ continue;
839
+ }
840
+ const parts = geminiParts(content, reporter);
841
+ let contentParts = [];
638
842
  let hasMedia = false;
843
+ let sawUserContent = false;
844
+ const flushContent = () => {
845
+ if (!sawUserContent) return;
846
+ if (contentParts.length === 0) {
847
+ out.push({ role: "user", content: "" });
848
+ } else if (hasMedia) {
849
+ out.push({ role: "user", content: contentParts });
850
+ } else {
851
+ const text = textOf(contentParts);
852
+ out.push({ role: "user", content: text });
853
+ }
854
+ contentParts = [];
855
+ hasMedia = false;
856
+ sawUserContent = false;
857
+ };
639
858
  for (const part of parts) {
640
859
  if (isRecord(part) && isRecord(part.functionResponse)) {
641
860
  const fr = part.functionResponse;
642
- const { id, matched } = resolveResponseId(fr, pending, reporter, generateId);
861
+ const response = Object.prototype.hasOwnProperty.call(fr, "response") ? fr.response : {};
862
+ const responseId = nonEmptyString2(fr.id) ? fr.id : void 0;
863
+ const matchedName = responseId ? pending.find((pendingCall) => pendingCall.providerId === responseId || pendingCall.id === responseId)?.name : void 0;
864
+ const name = geminiFunctionResponseName(fr.name, matchedName, reporter);
865
+ const { id, matched } = resolveResponseId({ id: fr.id, name }, pending, reporter, ids);
866
+ flushContent();
643
867
  out.push({
644
868
  role: "tool",
645
869
  tool_call_id: id,
646
- content: unwrapResponse(fr.response ?? {}),
647
- ...matched ? {} : { name: fr.name }
870
+ content: unwrapResponse(response, reporter, "Gemini", "functionResponse"),
871
+ ...matched ? {} : { name }
648
872
  });
649
873
  continue;
650
874
  }
@@ -652,6 +876,7 @@ function fromGemini(conversation, options = {}) {
652
876
  if (image) {
653
877
  contentParts.push(imageToOpenAI(image));
654
878
  hasMedia = true;
879
+ sawUserContent = true;
655
880
  continue;
656
881
  }
657
882
  const media = mediaFromGemini(part);
@@ -660,29 +885,74 @@ function fromGemini(conversation, options = {}) {
660
885
  if (openaiPart) {
661
886
  contentParts.push(openaiPart);
662
887
  hasMedia = true;
888
+ } else {
889
+ reporter.warn(
890
+ "dropped-content",
891
+ `A Gemini ${media.modality} ${media.source.kind} has no OpenAI Chat Completions equivalent; dropped.`
892
+ );
663
893
  }
894
+ sawUserContent = true;
664
895
  continue;
665
896
  }
666
897
  if (isRecord(part) && typeof part.text === "string") {
667
898
  contentParts.push({ type: "text", text: part.text });
899
+ sawUserContent = true;
900
+ continue;
668
901
  }
902
+ reporter.warn("dropped-content", "Dropped an unsupported Gemini user content part.");
669
903
  }
670
- if (contentParts.length > 0) {
671
- if (hasMedia) {
672
- out.push({ role: "user", content: contentParts });
673
- } else {
674
- const text = textOf(contentParts);
675
- out.push({ role: "user", content: text });
676
- }
677
- }
904
+ flushContent();
678
905
  }
679
906
  return out;
680
907
  }
681
- function resolveResponseId(response, pending, reporter, generateId) {
682
- if (response.id) {
683
- const index2 = pending.findIndex((p) => p.id === response.id);
684
- if (index2 >= 0) pending.splice(index2, 1);
685
- return { id: response.id, matched: index2 >= 0 };
908
+ function geminiParts(content, reporter) {
909
+ if (Array.isArray(content.parts)) return content.parts;
910
+ reporter.warn("dropped-content", "Dropped malformed Gemini content parts.");
911
+ return [];
912
+ }
913
+ function geminiFunctionResponseName(value, matchedName, reporter) {
914
+ if (matchedName && value === void 0) return matchedName;
915
+ if (matchedName && !isProviderFunctionName(value)) {
916
+ reporter.warn(
917
+ "dropped-metadata",
918
+ `Gemini functionResponse had a missing or invalid function name; used matching functionCall '${matchedName}'.`
919
+ );
920
+ return matchedName;
921
+ }
922
+ return providerFunctionName(value, reporter, "Gemini", "functionResponse");
923
+ }
924
+ function resolveResponseId(response, pending, reporter, ids) {
925
+ const responseId = nonEmptyString2(response.id) ? response.id : void 0;
926
+ if (response.id !== void 0 && typeof response.id !== "string") {
927
+ reporter.warn(
928
+ "dropped-metadata",
929
+ `Gemini functionResponse for '${response.name}' had a non-string id; ignored it.`
930
+ );
931
+ }
932
+ if (responseId) {
933
+ const index2 = pending.findIndex((p) => p.providerId === responseId || p.id === responseId);
934
+ if (index2 >= 0) {
935
+ const [match] = pending.splice(index2, 1);
936
+ if (response.name !== match.name) {
937
+ reporter.warn(
938
+ "dropped-metadata",
939
+ `Gemini functionResponse '${responseId}' name '${response.name}' differed from matching functionCall '${match.name}'; used the call id mapping.`
940
+ );
941
+ }
942
+ return { id: match.id, matched: true };
943
+ }
944
+ reporter.warn(
945
+ "unmapped-tool-result",
946
+ `Gemini functionResponse '${responseId}' for '${response.name}' had no matching call; kept the response id.`
947
+ );
948
+ const id2 = ids.claim(responseId, response.name);
949
+ if (id2 !== responseId) {
950
+ reporter.warn(
951
+ "generated-id",
952
+ `Gemini functionResponse '${responseId}' for '${response.name}' reused an existing id; generated '${id2}'.`
953
+ );
954
+ }
955
+ return { id: id2, matched: false };
686
956
  }
687
957
  const index = pending.findIndex((p) => p.name === response.name);
688
958
  if (index >= 0) {
@@ -690,13 +960,31 @@ function resolveResponseId(response, pending, reporter, generateId) {
690
960
  pending.splice(index, 1);
691
961
  return { id: id2, matched: true };
692
962
  }
693
- const id = generateId(response.name);
963
+ const id = ids.generate(response.name);
694
964
  reporter.warn(
695
965
  "unmapped-tool-result",
696
966
  `Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`
697
967
  );
698
968
  return { id, matched: false };
699
969
  }
970
+ function geminiProviderIds(conversation) {
971
+ const ids = [];
972
+ const root = isRecord(conversation) ? conversation : {};
973
+ const contents = Array.isArray(root.contents) ? root.contents : [];
974
+ for (const content of contents) {
975
+ if (!isRecord(content)) continue;
976
+ const parts = Array.isArray(content.parts) ? content.parts : [];
977
+ for (const part of parts) {
978
+ if (isRecord(part) && isRecord(part.functionCall) && nonEmptyString2(part.functionCall.id)) {
979
+ ids.push(part.functionCall.id);
980
+ }
981
+ if (isRecord(part) && isRecord(part.functionResponse) && nonEmptyString2(part.functionResponse.id)) {
982
+ ids.push(part.functionResponse.id);
983
+ }
984
+ }
985
+ }
986
+ return ids;
987
+ }
700
988
 
701
989
  // src/convert.ts
702
990
  function convert(conversation, route, options = {}) {
@@ -730,6 +1018,42 @@ function fromCanonical(canonical, to, options) {
730
1018
 
731
1019
  // src/response.ts
732
1020
  var num = (value) => typeof value === "number" ? value : 0;
1021
+ var nonEmptyString3 = (value) => typeof value === "string" && value.length > 0;
1022
+ function responseRoot(body, reporter, provider) {
1023
+ if (isRecord(body)) return body;
1024
+ reporter.warn("dropped-content", `Dropped malformed ${provider} response body; expected an object.`);
1025
+ return {};
1026
+ }
1027
+ function responseArrayField(root, field, reporter, context, noun) {
1028
+ const value = root[field];
1029
+ if (value === void 0) {
1030
+ reporter.warn("dropped-content", `${context} missing ${field} array; no ${noun} were read.`);
1031
+ return [];
1032
+ }
1033
+ if (!Array.isArray(value)) {
1034
+ reporter.warn("dropped-content", `Dropped malformed ${context} ${field}; expected an array.`);
1035
+ return [];
1036
+ }
1037
+ return value;
1038
+ }
1039
+ function firstResponseRecord(items, reporter, provider, noun) {
1040
+ if (items.length === 0) return {};
1041
+ if (isRecord(items[0])) return items[0];
1042
+ reporter.warn("dropped-content", `Dropped malformed ${provider} response ${noun}; expected an object.`);
1043
+ return {};
1044
+ }
1045
+ function responseRecordField(root, field, reporter, context, noun) {
1046
+ const value = root[field];
1047
+ if (value === void 0) {
1048
+ reporter.warn("dropped-content", `${context} missing ${field} object; no ${noun} were read.`);
1049
+ return {};
1050
+ }
1051
+ if (!isRecord(value)) {
1052
+ reporter.warn("dropped-content", `Dropped malformed ${context} ${field}; expected an object.`);
1053
+ return {};
1054
+ }
1055
+ return value;
1056
+ }
733
1057
  function buildMessage(text, toolCalls) {
734
1058
  const message = { role: "assistant", content: text ? text : null };
735
1059
  if (toolCalls.length > 0) message.tool_calls = toolCalls;
@@ -738,6 +1062,97 @@ function buildMessage(text, toolCalls) {
738
1062
  function finalReason(mapped, toolCalls) {
739
1063
  return toolCalls.length > 0 ? "tool_calls" : mapped;
740
1064
  }
1065
+ function normalizeProviderArguments(value, reporter, provider, part, fnName) {
1066
+ if (typeof value !== "string") return stringifyArgumentsObject(value, reporter, provider, part, fnName);
1067
+ const parsed = tryParseJson(value);
1068
+ if (parsed.ok && isRecord(parsed.value)) return value;
1069
+ reporter.warn(
1070
+ "invalid-json-arguments",
1071
+ `${provider} ${part} '${fnName}' had arguments that were not a JSON object string; used an empty object instead.`
1072
+ );
1073
+ return "{}";
1074
+ }
1075
+ function normalizeOpenAIToolCalls(value, reporter) {
1076
+ if (value === void 0) return [];
1077
+ if (!Array.isArray(value)) {
1078
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Chat Completions tool_calls; expected an array.");
1079
+ return [];
1080
+ }
1081
+ const toolCalls = [];
1082
+ const ids = createToolCallIdGenerator(
1083
+ value.flatMap(
1084
+ (call) => isRecord(call) && (call.type === void 0 || call.type === "function") && nonEmptyString3(call.id) ? [call.id] : []
1085
+ )
1086
+ );
1087
+ for (const call of value) {
1088
+ if (!isRecord(call)) {
1089
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Chat Completions tool_call; expected an object.");
1090
+ continue;
1091
+ }
1092
+ if (call.type !== void 0 && call.type !== "function") {
1093
+ reporter.warn(
1094
+ "dropped-content",
1095
+ `OpenAI Chat Completions tool_call type '${String(call.type)}' is not supported; dropped.`
1096
+ );
1097
+ continue;
1098
+ }
1099
+ const fn = isRecord(call.function) ? call.function : {};
1100
+ const name = providerFunctionName(fn.name, reporter, "OpenAI Chat Completions", "tool_call.function");
1101
+ const providedId = nonEmptyString3(call.id) ? call.id : void 0;
1102
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
1103
+ if (!providedId) {
1104
+ reporter.warn("generated-id", `OpenAI Chat Completions tool_call '${name}' had no id; generated '${id}'.`);
1105
+ } else if (id !== providedId) {
1106
+ reporter.warn(
1107
+ "generated-id",
1108
+ `OpenAI Chat Completions tool_call '${name}' reused id '${providedId}'; generated '${id}'.`
1109
+ );
1110
+ }
1111
+ const args = normalizeProviderArguments(
1112
+ fn.arguments,
1113
+ reporter,
1114
+ "OpenAI Chat Completions",
1115
+ "tool_call.function",
1116
+ name
1117
+ );
1118
+ toolCalls.push({ id, type: "function", function: { name, arguments: args } });
1119
+ }
1120
+ return toolCalls;
1121
+ }
1122
+ function normalizeOpenAIFunctionCall(value, reporter) {
1123
+ if (!isRecord(value)) return [];
1124
+ const name = providerFunctionName(value.name, reporter, "OpenAI Chat Completions", "function_call");
1125
+ const id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_0`;
1126
+ reporter.warn("generated-id", `OpenAI Chat Completions function_call '${name}' had no id; generated '${id}'.`);
1127
+ const args = normalizeProviderArguments(value.arguments, reporter, "OpenAI Chat Completions", "function_call", name);
1128
+ return [{ id, type: "function", function: { name, arguments: args } }];
1129
+ }
1130
+ function openAIChatContentText(content, reporter) {
1131
+ if (typeof content === "string") return content;
1132
+ if (content === null || content === void 0) return "";
1133
+ if (!Array.isArray(content)) {
1134
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Chat Completions response content.");
1135
+ return "";
1136
+ }
1137
+ const pieces = [];
1138
+ for (const part of content) {
1139
+ if (typeof part === "string") {
1140
+ pieces.push(part);
1141
+ } else if (isRecord(part) && typeof part.text === "string") {
1142
+ pieces.push(part.text);
1143
+ } else if (isRecord(part) && part.type === "refusal" && typeof part.refusal === "string") {
1144
+ pieces.push(part.refusal);
1145
+ } else {
1146
+ reporter.warn("dropped-content", "Dropped unsupported OpenAI Chat Completions response content part.");
1147
+ }
1148
+ }
1149
+ return pieces.join("");
1150
+ }
1151
+ function openAIChatMessageText(message, reporter) {
1152
+ const content = openAIChatContentText(message.content, reporter);
1153
+ if (content) return content;
1154
+ return typeof message.refusal === "string" ? message.refusal : content;
1155
+ }
741
1156
  var OPENAI_FINISH = {
742
1157
  stop: "stop",
743
1158
  length: "length",
@@ -745,16 +1160,27 @@ var OPENAI_FINISH = {
745
1160
  content_filter: "content_filter",
746
1161
  function_call: "tool_calls"
747
1162
  };
748
- function responseFromOpenAI(body) {
749
- const root = isRecord(body) ? body : {};
750
- const choice = Array.isArray(root.choices) && isRecord(root.choices[0]) ? root.choices[0] : {};
751
- const message = isRecord(choice.message) ? choice.message : {};
752
- const text = typeof message.content === "string" ? message.content : textOf(message.content);
753
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
1163
+ function responseFromOpenAI(body, options = {}) {
1164
+ const reporter = new Reporter(options);
1165
+ const isObjectBody = isRecord(body);
1166
+ const root = isObjectBody ? body : responseRoot(body, reporter, "OpenAI Chat Completions");
1167
+ const choices = isObjectBody ? responseArrayField(root, "choices", reporter, "OpenAI Chat Completions response", "choices") : [];
1168
+ const hasChoiceRecord = choices.length > 0 && isRecord(choices[0]);
1169
+ const choice = firstResponseRecord(choices, reporter, "OpenAI Chat Completions", "choice");
1170
+ const message = hasChoiceRecord ? responseRecordField(
1171
+ choice,
1172
+ "message",
1173
+ reporter,
1174
+ "OpenAI Chat Completions response choice",
1175
+ "message content or tool calls"
1176
+ ) : {};
1177
+ const text = openAIChatMessageText(message, reporter);
1178
+ const toolCalls = normalizeOpenAIToolCalls(message.tool_calls, reporter);
1179
+ const normalizedToolCalls = toolCalls.length > 0 ? toolCalls : normalizeOpenAIFunctionCall(message.function_call, reporter);
754
1180
  const usage = isRecord(root.usage) ? root.usage : {};
755
1181
  return {
756
- message: buildMessage(text, toolCalls),
757
- finishReason: finalReason(OPENAI_FINISH[String(choice.finish_reason)] ?? "unknown", toolCalls),
1182
+ message: buildMessage(text, normalizedToolCalls),
1183
+ finishReason: finalReason(OPENAI_FINISH[String(choice.finish_reason)] ?? "unknown", normalizedToolCalls),
758
1184
  usage: { inputTokens: num(usage.prompt_tokens), outputTokens: num(usage.completion_tokens) }
759
1185
  };
760
1186
  }
@@ -762,35 +1188,85 @@ var OPENAI_RESPONSES_INCOMPLETE = {
762
1188
  max_output_tokens: "length",
763
1189
  content_filter: "content_filter"
764
1190
  };
1191
+ function openAIResponsesContentText(content, reporter) {
1192
+ if (content === void 0) return "";
1193
+ if (!Array.isArray(content)) {
1194
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses message content.");
1195
+ return "";
1196
+ }
1197
+ const pieces = [];
1198
+ for (const part of content) {
1199
+ if (!isRecord(part)) {
1200
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses message content part.");
1201
+ } else if (typeof part.text === "string" && (part.type === "output_text" || part.type === "text")) {
1202
+ pieces.push(part.text);
1203
+ } else if (part.type === "refusal" && typeof part.refusal === "string") {
1204
+ pieces.push(part.refusal);
1205
+ } else {
1206
+ reporter.warn("dropped-content", "Dropped unsupported OpenAI Responses message content part.");
1207
+ }
1208
+ }
1209
+ return pieces.join("");
1210
+ }
765
1211
  function responseApiFinishReason(root) {
766
1212
  if (root.status === "completed") return "stop";
767
1213
  if (root.status !== "incomplete") return "unknown";
768
1214
  const details = isRecord(root.incomplete_details) ? root.incomplete_details : {};
769
1215
  return OPENAI_RESPONSES_INCOMPLETE[String(details.reason)] ?? "unknown";
770
1216
  }
771
- function responseFromOpenAIResponses(body) {
772
- const root = isRecord(body) ? body : {};
773
- const output = Array.isArray(root.output) ? root.output : [];
1217
+ function openAIResponsesOutput(root, reporter) {
1218
+ if (root.output === void 0) {
1219
+ reporter.warn(
1220
+ "dropped-content",
1221
+ "OpenAI Responses response missing top-level output array; no output items were read."
1222
+ );
1223
+ return [];
1224
+ }
1225
+ if (!Array.isArray(root.output)) {
1226
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses top-level output; expected an array.");
1227
+ return [];
1228
+ }
1229
+ return root.output;
1230
+ }
1231
+ function responseFromOpenAIResponses(body, options = {}) {
1232
+ const reporter = new Reporter(options);
1233
+ const isObjectBody = isRecord(body);
1234
+ const root = isObjectBody ? body : responseRoot(body, reporter, "OpenAI Responses");
1235
+ const output = isObjectBody ? openAIResponsesOutput(root, reporter) : [];
774
1236
  const textPieces = [];
775
1237
  const toolCalls = [];
776
- let counter = 0;
1238
+ const ids = createToolCallIdGenerator(
1239
+ output.flatMap((item) => {
1240
+ if (!isRecord(item) || item.type !== "function_call") return [];
1241
+ if (nonEmptyString3(item.call_id)) return [item.call_id];
1242
+ return nonEmptyString3(item.id) ? [item.id] : [];
1243
+ })
1244
+ );
777
1245
  for (const item of output) {
778
- if (!isRecord(item)) continue;
1246
+ if (!isRecord(item)) {
1247
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses output item.");
1248
+ continue;
1249
+ }
779
1250
  if (item.type === "message") {
780
- const content = Array.isArray(item.content) ? item.content : [];
781
- for (const part of content) {
782
- if (!isRecord(part)) continue;
783
- if (typeof part.text === "string" && (part.type === "output_text" || part.type === "text")) {
784
- textPieces.push(part.text);
785
- } else if (part.type === "refusal" && typeof part.refusal === "string") {
786
- textPieces.push(part.refusal);
787
- }
1251
+ textPieces.push(openAIResponsesContentText(item.content, reporter));
1252
+ } else if (item.type === "function_call") {
1253
+ const name = providerFunctionName(item.name, reporter, "OpenAI Responses", "function_call");
1254
+ const callId = nonEmptyString3(item.call_id) ? item.call_id : void 0;
1255
+ const itemId = nonEmptyString3(item.id) ? item.id : void 0;
1256
+ const id = callId ?? itemId;
1257
+ const toolCallId = id ? ids.claim(id, name) : ids.generate(name);
1258
+ if (!callId && !itemId) {
1259
+ reporter.warn("generated-id", `OpenAI Responses function_call '${name}' had no id; generated '${toolCallId}'.`);
1260
+ } else if (toolCallId !== id) {
1261
+ reporter.warn(
1262
+ "generated-id",
1263
+ `OpenAI Responses function_call '${name}' reused id '${id}'; generated '${toolCallId}'.`
1264
+ );
788
1265
  }
789
- } else if (item.type === "function_call" && typeof item.name === "string") {
790
- const name = item.name;
791
- const id = typeof item.call_id === "string" ? item.call_id : typeof item.id === "string" ? item.id : `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
792
- const args = typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {});
793
- toolCalls.push({ id, type: "function", function: { name, arguments: args } });
1266
+ const args = normalizeProviderArguments(item.arguments, reporter, "OpenAI Responses", "function_call", name);
1267
+ toolCalls.push({ id: toolCallId, type: "function", function: { name, arguments: args } });
1268
+ } else {
1269
+ reporter.warn("dropped-content", `Dropped unsupported OpenAI Responses output item '${String(item.type)}'.`);
794
1270
  }
795
1271
  }
796
1272
  const usage = isRecord(root.usage) ? root.usage : {};
@@ -808,21 +1284,47 @@ var ANTHROPIC_FINISH = {
808
1284
  refusal: "content_filter",
809
1285
  pause_turn: "unknown"
810
1286
  };
811
- function responseFromAnthropic(body) {
812
- const root = isRecord(body) ? body : {};
813
- const blocks = Array.isArray(root.content) ? root.content : [];
1287
+ function responseFromAnthropic(body, options = {}) {
1288
+ const reporter = new Reporter(options);
1289
+ const isObjectBody = isRecord(body);
1290
+ const root = isObjectBody ? body : responseRoot(body, reporter, "Anthropic");
1291
+ const blocks = isObjectBody ? responseArrayField(root, "content", reporter, "Anthropic response", "content blocks") : [];
814
1292
  const textPieces = [];
815
1293
  const toolCalls = [];
1294
+ const ids = createToolCallIdGenerator(
1295
+ blocks.flatMap(
1296
+ (block) => isRecord(block) && block.type === "tool_use" && nonEmptyString3(block.id) ? [block.id] : []
1297
+ )
1298
+ );
816
1299
  for (const block of blocks) {
817
- if (!isRecord(block)) continue;
1300
+ if (!isRecord(block)) {
1301
+ reporter.warn("dropped-content", "Dropped a malformed Anthropic response content block.");
1302
+ continue;
1303
+ }
818
1304
  if (block.type === "text" && typeof block.text === "string") {
819
1305
  textPieces.push(block.text);
820
- } else if (block.type === "tool_use" && typeof block.name === "string") {
1306
+ } else if (block.type === "tool_use") {
1307
+ const name = providerFunctionName(block.name, reporter, "Anthropic", "tool_use");
1308
+ const providedId = nonEmptyString3(block.id) ? block.id : void 0;
1309
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
1310
+ if (!providedId) {
1311
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' had no id; generated '${id}'.`);
1312
+ } else if (id !== providedId) {
1313
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' reused id '${providedId}'; generated '${id}'.`);
1314
+ }
821
1315
  toolCalls.push({
822
- id: typeof block.id === "string" ? block.id : "",
1316
+ id,
823
1317
  type: "function",
824
- function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) }
1318
+ function: {
1319
+ name,
1320
+ arguments: stringifyArgumentsObject(block.input, reporter, "Anthropic", "tool_use", name)
1321
+ }
825
1322
  });
1323
+ } else {
1324
+ reporter.warn(
1325
+ "dropped-content",
1326
+ `Anthropic response content block '${String(block.type)}' is not supported; dropped.`
1327
+ );
826
1328
  }
827
1329
  }
828
1330
  const usage = isRecord(root.usage) ? root.usage : {};
@@ -837,30 +1339,63 @@ var GEMINI_FINISH = {
837
1339
  MAX_TOKENS: "length",
838
1340
  SAFETY: "content_filter",
839
1341
  RECITATION: "content_filter",
840
- MALFORMED_FUNCTION_CALL: "content_filter"
1342
+ BLOCKLIST: "content_filter",
1343
+ PROHIBITED_CONTENT: "content_filter",
1344
+ SPII: "content_filter",
1345
+ MALFORMED_FUNCTION_CALL: "content_filter",
1346
+ MODEL_ARMOR: "content_filter",
1347
+ IMAGE_SAFETY: "content_filter",
1348
+ IMAGE_PROHIBITED_CONTENT: "content_filter",
1349
+ IMAGE_RECITATION: "content_filter"
841
1350
  };
842
1351
  function responseFromGemini(body, options = {}) {
843
1352
  const reporter = new Reporter(options);
844
- const root = isRecord(body) ? body : {};
845
- const candidate = Array.isArray(root.candidates) && isRecord(root.candidates[0]) ? root.candidates[0] : {};
1353
+ const isObjectBody = isRecord(body);
1354
+ const root = isObjectBody ? body : responseRoot(body, reporter, "Gemini");
1355
+ const candidates = isObjectBody ? responseArrayField(root, "candidates", reporter, "Gemini response", "candidates") : [];
1356
+ const hasCandidateRecord = candidates.length > 0 && isRecord(candidates[0]);
1357
+ const candidate = firstResponseRecord(candidates, reporter, "Gemini", "candidate");
846
1358
  const content = isRecord(candidate.content) ? candidate.content : {};
847
- const parts = Array.isArray(content.parts) ? content.parts : [];
1359
+ if (hasCandidateRecord && !isRecord(candidate.content)) {
1360
+ reporter.warn("dropped-content", "Dropped malformed Gemini response candidate content; expected an object.");
1361
+ }
1362
+ const parts = hasCandidateRecord && isRecord(candidate.content) ? responseArrayField(content, "parts", reporter, "Gemini response candidate content", "content parts") : [];
848
1363
  const textPieces = [];
849
1364
  const toolCalls = [];
850
- let counter = 0;
1365
+ const ids = createToolCallIdGenerator(
1366
+ parts.flatMap(
1367
+ (part) => isRecord(part) && isRecord(part.functionCall) && nonEmptyString3(part.functionCall.id) ? [part.functionCall.id] : []
1368
+ )
1369
+ );
851
1370
  for (const part of parts) {
852
- if (!isRecord(part)) continue;
1371
+ if (!isRecord(part)) {
1372
+ reporter.warn("dropped-content", "Dropped a malformed Gemini response content part.");
1373
+ continue;
1374
+ }
853
1375
  if (isRecord(part.functionCall)) {
854
1376
  const call = part.functionCall;
855
- const name = typeof call.name === "string" ? call.name : "function";
856
- let id = call.id;
857
- if (!id) {
858
- id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
1377
+ const name = providerFunctionName(call.name, reporter, "Gemini", "functionCall");
1378
+ const providedId = nonEmptyString3(call.id) ? call.id : void 0;
1379
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
1380
+ if (!providedId) {
859
1381
  reporter.warn("generated-id", `Gemini functionCall '${name}' had no id; generated '${id}'.`);
1382
+ } else if (id !== providedId) {
1383
+ reporter.warn("generated-id", `Gemini functionCall '${name}' reused id '${providedId}'; generated '${id}'.`);
860
1384
  }
861
- toolCalls.push({ id, type: "function", function: { name, arguments: JSON.stringify(call.args ?? {}) } });
1385
+ toolCalls.push({
1386
+ id,
1387
+ type: "function",
1388
+ function: {
1389
+ name,
1390
+ arguments: stringifyArgumentsObject(call.args, reporter, "Gemini", "functionCall", name)
1391
+ }
1392
+ });
1393
+ } else if (part.thought === true) {
1394
+ reporter.warn("dropped-content", "Dropped a Gemini thought (reasoning) response part.");
862
1395
  } else if (typeof part.text === "string") {
863
1396
  textPieces.push(part.text);
1397
+ } else {
1398
+ reporter.warn("dropped-content", "Dropped an unsupported Gemini response content part.");
864
1399
  }
865
1400
  }
866
1401
  const usage = isRecord(root.usageMetadata) ? root.usageMetadata : {};
@@ -873,17 +1408,31 @@ function responseFromGemini(body, options = {}) {
873
1408
  function normalizeResponse(body, route, options = {}) {
874
1409
  switch (route.from) {
875
1410
  case "openai":
876
- return responseFromOpenAI(body);
1411
+ return responseFromOpenAI(body, options);
877
1412
  case "openai-responses":
878
- return responseFromOpenAIResponses(body);
1413
+ return responseFromOpenAIResponses(body, options);
879
1414
  case "anthropic":
880
- return responseFromAnthropic(body);
1415
+ return responseFromAnthropic(body, options);
881
1416
  case "gemini":
882
1417
  return responseFromGemini(body, options);
883
1418
  default:
884
1419
  throw new Error(`Unknown source provider: ${String(route.from)}`);
885
1420
  }
886
1421
  }
1422
+
1423
+ // src/types.ts
1424
+ var warningCodes = Object.freeze([
1425
+ "generated-id",
1426
+ "unmapped-tool-result",
1427
+ "merged-role",
1428
+ "dropped-content",
1429
+ "dropped-metadata",
1430
+ "invalid-json-arguments",
1431
+ "system-midstream",
1432
+ "gemini-url-image",
1433
+ "gemini-url-media",
1434
+ "unsupported-modality"
1435
+ ]);
887
1436
  // Annotate the CommonJS export names for ESM import in node:
888
1437
  0 && (module.exports = {
889
1438
  convert,
@@ -897,6 +1446,7 @@ function normalizeResponse(body, route, options = {}) {
897
1446
  responseFromOpenAIResponses,
898
1447
  toAnthropic,
899
1448
  toDataUrl,
900
- toGemini
1449
+ toGemini,
1450
+ warningCodes
901
1451
  });
902
1452
  //# sourceMappingURL=index.cjs.map