llm-messages 0.5.1 → 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.js CHANGED
@@ -17,6 +17,7 @@ function textOf(content) {
17
17
  return content.map((part) => {
18
18
  if (typeof part === "string") return part;
19
19
  if (isRecord(part) && typeof part.text === "string") return part.text;
20
+ if (isRecord(part) && part.type === "refusal" && typeof part.refusal === "string") return part.refusal;
20
21
  return "";
21
22
  }).join("");
22
23
  }
@@ -38,17 +39,83 @@ function parseArguments(args, reporter, fnName) {
38
39
  );
39
40
  return {};
40
41
  }
42
+ function stringifyArgumentsObject(value, reporter, provider, part, fnName) {
43
+ if (value === void 0) return "{}";
44
+ if (isRecord(value)) {
45
+ try {
46
+ return JSON.stringify(value);
47
+ } catch {
48
+ reporter.warn(
49
+ "invalid-json-arguments",
50
+ `${provider} ${part} '${fnName}' had arguments that could not be serialized as JSON; used an empty object instead.`
51
+ );
52
+ return "{}";
53
+ }
54
+ }
55
+ reporter.warn(
56
+ "invalid-json-arguments",
57
+ `${provider} ${part} '${fnName}' had arguments that were not an object; used an empty object instead.`
58
+ );
59
+ return "{}";
60
+ }
61
+ var OPENAI_FUNCTION_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
62
+ function isProviderFunctionName(value) {
63
+ return typeof value === "string" && OPENAI_FUNCTION_NAME.test(value);
64
+ }
65
+ function providerFunctionName(value, reporter, provider, part) {
66
+ if (isProviderFunctionName(value)) return value;
67
+ reporter.warn(
68
+ "dropped-metadata",
69
+ `${provider} ${part} had a missing or invalid function name; used 'unknown_function'.`
70
+ );
71
+ return "unknown_function";
72
+ }
73
+ function createToolCallIdGenerator(reservedIds = []) {
74
+ const reserved = new Set(reservedIds);
75
+ const used = /* @__PURE__ */ new Set();
76
+ let counter = 0;
77
+ const generate = (name) => {
78
+ let id;
79
+ do {
80
+ id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
81
+ } while (used.has(id) || reserved.has(id));
82
+ used.add(id);
83
+ return id;
84
+ };
85
+ return {
86
+ claim(id, name) {
87
+ if (used.has(id)) return generate(name);
88
+ used.add(id);
89
+ reserved.delete(id);
90
+ return id;
91
+ },
92
+ generate(name) {
93
+ return generate(name);
94
+ }
95
+ };
96
+ }
41
97
  function wrapResponse(content) {
42
98
  const parsed = tryParseJson(content);
43
99
  if (parsed.ok && isRecord(parsed.value)) return parsed.value;
44
100
  return { result: content };
45
101
  }
46
- function unwrapResponse(response) {
47
- const keys = Object.keys(response);
48
- if (keys.length === 1 && keys[0] === "result" && typeof response.result === "string") {
49
- return response.result;
102
+ function unwrapResponse(response, reporter, provider = "Provider", part = "response") {
103
+ if (response === void 0) return "{}";
104
+ if (isRecord(response)) {
105
+ const keys = Object.keys(response);
106
+ if (keys.length === 1 && keys[0] === "result" && typeof response.result === "string") {
107
+ return response.result;
108
+ }
109
+ }
110
+ try {
111
+ return JSON.stringify(response) ?? "{}";
112
+ } catch {
113
+ reporter?.warn(
114
+ "dropped-content",
115
+ `${provider} ${part} response could not be serialized as JSON; used an empty object instead.`
116
+ );
117
+ return "{}";
50
118
  }
51
- return JSON.stringify(response);
52
119
  }
53
120
 
54
121
  // src/image.ts
@@ -267,6 +334,7 @@ function splitSystem(messages, reporter) {
267
334
  }
268
335
 
269
336
  // src/providers/anthropic.ts
337
+ var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
270
338
  function toAnthropic(messages, options = {}) {
271
339
  const reporter = new Reporter(options);
272
340
  const { system, rest } = splitSystem(messages, reporter);
@@ -328,7 +396,7 @@ function userContent(content, reporter) {
328
396
  }
329
397
  reporter.warn("dropped-content", "Dropped an unsupported user content part.");
330
398
  }
331
- return blocks;
399
+ return blocks.length > 0 ? blocks : "";
332
400
  }
333
401
  function assistantContent(message, reporter) {
334
402
  warnDroppedName("Assistant", message.name, "Anthropic", reporter);
@@ -368,17 +436,59 @@ function mergeConsecutive(messages, reporter) {
368
436
  return result;
369
437
  }
370
438
  function asBlocks(content) {
371
- if (typeof content !== "string") return content;
439
+ if (Array.isArray(content)) return content.filter(isRecord);
440
+ if (typeof content !== "string") return [];
372
441
  return content ? [{ type: "text", text: content }] : [];
373
442
  }
443
+ function warnMalformedBlocks(content, reporter) {
444
+ if (Array.isArray(content)) {
445
+ for (const block of content) {
446
+ if (!isRecord(block)) {
447
+ reporter.warn("dropped-content", "Dropped a malformed Anthropic content block.");
448
+ }
449
+ }
450
+ return;
451
+ }
452
+ if (typeof content !== "string") {
453
+ reporter.warn("dropped-content", "Dropped malformed Anthropic message content.");
454
+ }
455
+ }
456
+ function isSupportedUserBlock(block) {
457
+ return block.type === "text" && typeof block.text === "string" || block.type === "tool_result" || imageFromAnthropic(block) !== null || mediaFromAnthropic(block) !== null;
458
+ }
459
+ function isSupportedAssistantBlock(block) {
460
+ return block.type === "text" && typeof block.text === "string" || block.type === "tool_use";
461
+ }
462
+ function warnUnsupportedBlocks(blocks, role, reporter) {
463
+ const isSupported = role === "user" ? isSupportedUserBlock : isSupportedAssistantBlock;
464
+ for (const block of blocks) {
465
+ if (isSupported(block)) continue;
466
+ reporter.warn("dropped-content", `Dropped unsupported Anthropic ${role} content block '${String(block.type)}'.`);
467
+ }
468
+ }
374
469
  function fromAnthropic(conversation, options = {}) {
375
470
  const reporter = new Reporter(options);
376
471
  const out = [];
377
- if (conversation.system) {
378
- out.push({ role: "system", content: textOf(conversation.system) });
472
+ const root = isRecord(conversation) ? conversation : {};
473
+ const system = textOf(root.system);
474
+ if (system) {
475
+ out.push({ role: "system", content: system });
379
476
  }
380
- for (const message of conversation.messages) {
477
+ const messages = Array.isArray(root.messages) ? root.messages : [];
478
+ const ids = createToolCallIdGenerator(anthropicProviderIds(root));
479
+ const pendingToolUseIds = /* @__PURE__ */ new Map();
480
+ for (const message of messages) {
481
+ if (!isRecord(message)) {
482
+ reporter.warn("dropped-content", "Dropped a malformed Anthropic message.");
483
+ continue;
484
+ }
485
+ if (message.role !== "user" && message.role !== "assistant") {
486
+ reporter.warn("dropped-content", `Dropped an Anthropic message with unsupported role '${String(message.role)}'.`);
487
+ continue;
488
+ }
489
+ warnMalformedBlocks(message.content, reporter);
381
490
  const blocks = asBlocks(message.content);
491
+ warnUnsupportedBlocks(blocks, message.role, reporter);
382
492
  if (message.role === "user") {
383
493
  if (blocks.length === 0) {
384
494
  out.push({ role: "user", content: "" });
@@ -397,9 +507,28 @@ function fromAnthropic(conversation, options = {}) {
397
507
  continue;
398
508
  }
399
509
  flushContent();
510
+ const rawToolUseId = block.tool_use_id;
511
+ const hasToolUseId = nonEmptyString(rawToolUseId);
512
+ const matchedToolCallId = hasToolUseId ? takePendingToolUseId(pendingToolUseIds, rawToolUseId) : void 0;
513
+ const toolCallId = matchedToolCallId ?? (hasToolUseId ? ids.claim(rawToolUseId, "tool_result") : ids.generate("tool_result"));
514
+ if (!hasToolUseId) {
515
+ reporter.warn(
516
+ "unmapped-tool-result",
517
+ `Anthropic tool_result had no usable tool_use_id; generated '${toolCallId}'.`
518
+ );
519
+ } else if (!matchedToolCallId) {
520
+ 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}'.`;
521
+ reporter.warn("unmapped-tool-result", unmappedMessage);
522
+ if (toolCallId !== rawToolUseId) {
523
+ reporter.warn(
524
+ "generated-id",
525
+ `Anthropic tool_result '${rawToolUseId}' reused an existing id; generated '${toolCallId}'.`
526
+ );
527
+ }
528
+ }
400
529
  out.push({
401
530
  role: "tool",
402
- tool_call_id: String(block.tool_use_id ?? ""),
531
+ tool_call_id: toolCallId,
403
532
  content: textOf(block.content),
404
533
  ...typeof block.is_error === "boolean" ? { is_error: block.is_error } : {}
405
534
  });
@@ -413,13 +542,57 @@ function fromAnthropic(conversation, options = {}) {
413
542
  if (toolUses.length > 0) {
414
543
  assistant.tool_calls = toolUses.map((block) => {
415
544
  const b = block;
416
- return { id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } };
545
+ const name = providerFunctionName(b.name, reporter, "Anthropic", "tool_use");
546
+ const providedId = nonEmptyString(b.id) ? b.id : void 0;
547
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
548
+ if (!providedId) {
549
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' had no id; generated '${id}'.`);
550
+ } else if (id !== providedId) {
551
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' reused id '${providedId}'; generated '${id}'.`);
552
+ }
553
+ if (providedId) pushPendingToolUseId(pendingToolUseIds, providedId, id);
554
+ return {
555
+ id,
556
+ type: "function",
557
+ function: {
558
+ name,
559
+ arguments: stringifyArgumentsObject(b.input, reporter, "Anthropic", "tool_use", name)
560
+ }
561
+ };
417
562
  });
418
563
  }
419
564
  out.push(assistant);
420
565
  }
421
566
  return out;
422
567
  }
568
+ function pushPendingToolUseId(pending, providerId, normalizedId) {
569
+ const ids = pending.get(providerId);
570
+ if (ids) ids.push(normalizedId);
571
+ else pending.set(providerId, [normalizedId]);
572
+ }
573
+ function takePendingToolUseId(pending, providerId) {
574
+ const ids = pending.get(providerId);
575
+ const id = ids?.shift();
576
+ if (ids?.length === 0) pending.delete(providerId);
577
+ return id;
578
+ }
579
+ function anthropicProviderIds(conversation) {
580
+ const ids = [];
581
+ const root = isRecord(conversation) ? conversation : {};
582
+ const messages = Array.isArray(root.messages) ? root.messages : [];
583
+ for (const message of messages) {
584
+ if (!isRecord(message)) continue;
585
+ for (const block of asBlocks(message.content)) {
586
+ if (block.type === "tool_use" && nonEmptyString(block.id)) {
587
+ ids.push(block.id);
588
+ }
589
+ if (block.type === "tool_result" && nonEmptyString(block.tool_use_id)) {
590
+ ids.push(block.tool_use_id);
591
+ }
592
+ }
593
+ }
594
+ return ids;
595
+ }
423
596
  function userContentToOpenAI(blocks, reporter) {
424
597
  const hasMedia = blocks.some((block) => imageFromAnthropic(block) !== null || mediaFromAnthropic(block) !== null);
425
598
  if (!hasMedia) return textOf(blocks);
@@ -441,10 +614,11 @@ function userContentToOpenAI(blocks, reporter) {
441
614
  parts.push({ type: "text", text: block.text });
442
615
  }
443
616
  }
444
- return parts;
617
+ return parts.length > 0 ? parts : "";
445
618
  }
446
619
 
447
620
  // src/providers/gemini.ts
621
+ var nonEmptyString2 = (value) => typeof value === "string" && value.length > 0;
448
622
  function toGemini(messages, options = {}) {
449
623
  const reporter = new Reporter(options);
450
624
  const { system, rest } = splitSystem(messages, reporter);
@@ -475,8 +649,9 @@ function toGemini(messages, options = {}) {
475
649
  `Tool message name '${tool.name}' differs from matching tool call '${matchingName}'; used the tool-call function name for Gemini.`
476
650
  );
477
651
  }
478
- const name = matchingName ?? tool.name;
479
- if (!name) {
652
+ const hasStandaloneName = nonEmptyString2(tool.name);
653
+ const name = matchingName ?? (hasStandaloneName ? tool.name : tool.tool_call_id);
654
+ if (!matchingName && !hasStandaloneName) {
480
655
  reporter.warn(
481
656
  "unmapped-tool-result",
482
657
  `Tool result '${tool.tool_call_id}' has no matching call; used the id as the function name.`
@@ -564,31 +739,51 @@ function mergeConsecutive2(contents, reporter) {
564
739
  function fromGemini(conversation, options = {}) {
565
740
  const reporter = new Reporter(options);
566
741
  const out = [];
567
- if (conversation.systemInstruction) {
568
- const text = textOf(conversation.systemInstruction.parts);
742
+ const root = isRecord(conversation) ? conversation : {};
743
+ const systemInstruction = isRecord(root.systemInstruction) ? root.systemInstruction : void 0;
744
+ if (systemInstruction) {
745
+ const text = textOf(systemInstruction.parts);
569
746
  if (text) out.push({ role: "system", content: text });
570
747
  }
571
748
  const pending = [];
572
- let counter = 0;
573
- const generateId = (name) => `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
574
- for (const content of conversation.contents) {
575
- const parts = Array.isArray(content.parts) ? content.parts : [];
749
+ const contents = Array.isArray(root.contents) ? root.contents : [];
750
+ const ids = createToolCallIdGenerator(geminiProviderIds(root));
751
+ for (const content of contents) {
752
+ if (!isRecord(content)) {
753
+ reporter.warn("dropped-content", "Dropped a malformed Gemini content entry.");
754
+ continue;
755
+ }
576
756
  if (content.role === "model") {
757
+ const parts2 = geminiParts(content, reporter);
577
758
  const textPieces = [];
578
759
  const toolCalls = [];
579
- for (const part of parts) {
760
+ for (const part of parts2) {
580
761
  if (isRecord(part) && isRecord(part.functionCall)) {
581
762
  const fc = part.functionCall;
582
- const id = fc.id ?? generateId(fc.name);
583
- if (!fc.id) reporter.warn("generated-id", `Gemini functionCall '${fc.name}' had no id; generated '${id}'.`);
763
+ const name = providerFunctionName(fc.name, reporter, "Gemini", "functionCall");
764
+ const providedId = nonEmptyString2(fc.id) ? fc.id : void 0;
765
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
766
+ if (!providedId) {
767
+ reporter.warn("generated-id", `Gemini functionCall '${name}' had no id; generated '${id}'.`);
768
+ } else if (id !== providedId) {
769
+ reporter.warn(
770
+ "generated-id",
771
+ `Gemini functionCall '${name}' reused id '${providedId}'; generated '${id}'.`
772
+ );
773
+ }
584
774
  toolCalls.push({
585
775
  id,
586
776
  type: "function",
587
- function: { name: fc.name, arguments: JSON.stringify(fc.args ?? {}) }
777
+ function: {
778
+ name,
779
+ arguments: stringifyArgumentsObject(fc.args, reporter, "Gemini", "functionCall", name)
780
+ }
588
781
  });
589
- pending.push({ id, name: fc.name });
782
+ pending.push({ id, name, ...providedId ? { providerId: providedId } : {} });
590
783
  } else if (isRecord(part) && typeof part.text === "string") {
591
784
  textPieces.push(part.text);
785
+ } else {
786
+ reporter.warn("dropped-content", "Dropped an unsupported Gemini model content part.");
592
787
  }
593
788
  }
594
789
  const text = textPieces.join("");
@@ -597,17 +792,45 @@ function fromGemini(conversation, options = {}) {
597
792
  out.push(assistant);
598
793
  continue;
599
794
  }
600
- const contentParts = [];
795
+ if (content.role !== void 0 && content.role !== "user") {
796
+ reporter.warn(
797
+ "dropped-content",
798
+ `Dropped a Gemini content entry with unsupported role '${String(content.role)}'.`
799
+ );
800
+ continue;
801
+ }
802
+ const parts = geminiParts(content, reporter);
803
+ let contentParts = [];
601
804
  let hasMedia = false;
805
+ let sawUserContent = false;
806
+ const flushContent = () => {
807
+ if (!sawUserContent) return;
808
+ if (contentParts.length === 0) {
809
+ out.push({ role: "user", content: "" });
810
+ } else if (hasMedia) {
811
+ out.push({ role: "user", content: contentParts });
812
+ } else {
813
+ const text = textOf(contentParts);
814
+ out.push({ role: "user", content: text });
815
+ }
816
+ contentParts = [];
817
+ hasMedia = false;
818
+ sawUserContent = false;
819
+ };
602
820
  for (const part of parts) {
603
821
  if (isRecord(part) && isRecord(part.functionResponse)) {
604
822
  const fr = part.functionResponse;
605
- const { id, matched } = resolveResponseId(fr, pending, reporter, generateId);
823
+ const response = Object.prototype.hasOwnProperty.call(fr, "response") ? fr.response : {};
824
+ const responseId = nonEmptyString2(fr.id) ? fr.id : void 0;
825
+ const matchedName = responseId ? pending.find((pendingCall) => pendingCall.providerId === responseId || pendingCall.id === responseId)?.name : void 0;
826
+ const name = geminiFunctionResponseName(fr.name, matchedName, reporter);
827
+ const { id, matched } = resolveResponseId({ id: fr.id, name }, pending, reporter, ids);
828
+ flushContent();
606
829
  out.push({
607
830
  role: "tool",
608
831
  tool_call_id: id,
609
- content: unwrapResponse(fr.response ?? {}),
610
- ...matched ? {} : { name: fr.name }
832
+ content: unwrapResponse(response, reporter, "Gemini", "functionResponse"),
833
+ ...matched ? {} : { name }
611
834
  });
612
835
  continue;
613
836
  }
@@ -615,6 +838,7 @@ function fromGemini(conversation, options = {}) {
615
838
  if (image) {
616
839
  contentParts.push(imageToOpenAI(image));
617
840
  hasMedia = true;
841
+ sawUserContent = true;
618
842
  continue;
619
843
  }
620
844
  const media = mediaFromGemini(part);
@@ -623,29 +847,74 @@ function fromGemini(conversation, options = {}) {
623
847
  if (openaiPart) {
624
848
  contentParts.push(openaiPart);
625
849
  hasMedia = true;
850
+ } else {
851
+ reporter.warn(
852
+ "dropped-content",
853
+ `A Gemini ${media.modality} ${media.source.kind} has no OpenAI Chat Completions equivalent; dropped.`
854
+ );
626
855
  }
856
+ sawUserContent = true;
627
857
  continue;
628
858
  }
629
859
  if (isRecord(part) && typeof part.text === "string") {
630
860
  contentParts.push({ type: "text", text: part.text });
861
+ sawUserContent = true;
862
+ continue;
631
863
  }
864
+ reporter.warn("dropped-content", "Dropped an unsupported Gemini user content part.");
632
865
  }
633
- if (contentParts.length > 0) {
634
- if (hasMedia) {
635
- out.push({ role: "user", content: contentParts });
636
- } else {
637
- const text = textOf(contentParts);
638
- out.push({ role: "user", content: text });
639
- }
640
- }
866
+ flushContent();
641
867
  }
642
868
  return out;
643
869
  }
644
- function resolveResponseId(response, pending, reporter, generateId) {
645
- if (response.id) {
646
- const index2 = pending.findIndex((p) => p.id === response.id);
647
- if (index2 >= 0) pending.splice(index2, 1);
648
- return { id: response.id, matched: index2 >= 0 };
870
+ function geminiParts(content, reporter) {
871
+ if (Array.isArray(content.parts)) return content.parts;
872
+ reporter.warn("dropped-content", "Dropped malformed Gemini content parts.");
873
+ return [];
874
+ }
875
+ function geminiFunctionResponseName(value, matchedName, reporter) {
876
+ if (matchedName && value === void 0) return matchedName;
877
+ if (matchedName && !isProviderFunctionName(value)) {
878
+ reporter.warn(
879
+ "dropped-metadata",
880
+ `Gemini functionResponse had a missing or invalid function name; used matching functionCall '${matchedName}'.`
881
+ );
882
+ return matchedName;
883
+ }
884
+ return providerFunctionName(value, reporter, "Gemini", "functionResponse");
885
+ }
886
+ function resolveResponseId(response, pending, reporter, ids) {
887
+ const responseId = nonEmptyString2(response.id) ? response.id : void 0;
888
+ if (response.id !== void 0 && typeof response.id !== "string") {
889
+ reporter.warn(
890
+ "dropped-metadata",
891
+ `Gemini functionResponse for '${response.name}' had a non-string id; ignored it.`
892
+ );
893
+ }
894
+ if (responseId) {
895
+ const index2 = pending.findIndex((p) => p.providerId === responseId || p.id === responseId);
896
+ if (index2 >= 0) {
897
+ const [match] = pending.splice(index2, 1);
898
+ if (response.name !== match.name) {
899
+ reporter.warn(
900
+ "dropped-metadata",
901
+ `Gemini functionResponse '${responseId}' name '${response.name}' differed from matching functionCall '${match.name}'; used the call id mapping.`
902
+ );
903
+ }
904
+ return { id: match.id, matched: true };
905
+ }
906
+ reporter.warn(
907
+ "unmapped-tool-result",
908
+ `Gemini functionResponse '${responseId}' for '${response.name}' had no matching call; kept the response id.`
909
+ );
910
+ const id2 = ids.claim(responseId, response.name);
911
+ if (id2 !== responseId) {
912
+ reporter.warn(
913
+ "generated-id",
914
+ `Gemini functionResponse '${responseId}' for '${response.name}' reused an existing id; generated '${id2}'.`
915
+ );
916
+ }
917
+ return { id: id2, matched: false };
649
918
  }
650
919
  const index = pending.findIndex((p) => p.name === response.name);
651
920
  if (index >= 0) {
@@ -653,13 +922,31 @@ function resolveResponseId(response, pending, reporter, generateId) {
653
922
  pending.splice(index, 1);
654
923
  return { id: id2, matched: true };
655
924
  }
656
- const id = generateId(response.name);
925
+ const id = ids.generate(response.name);
657
926
  reporter.warn(
658
927
  "unmapped-tool-result",
659
928
  `Gemini functionResponse for '${response.name}' had no matching call; generated '${id}'.`
660
929
  );
661
930
  return { id, matched: false };
662
931
  }
932
+ function geminiProviderIds(conversation) {
933
+ const ids = [];
934
+ const root = isRecord(conversation) ? conversation : {};
935
+ const contents = Array.isArray(root.contents) ? root.contents : [];
936
+ for (const content of contents) {
937
+ if (!isRecord(content)) continue;
938
+ const parts = Array.isArray(content.parts) ? content.parts : [];
939
+ for (const part of parts) {
940
+ if (isRecord(part) && isRecord(part.functionCall) && nonEmptyString2(part.functionCall.id)) {
941
+ ids.push(part.functionCall.id);
942
+ }
943
+ if (isRecord(part) && isRecord(part.functionResponse) && nonEmptyString2(part.functionResponse.id)) {
944
+ ids.push(part.functionResponse.id);
945
+ }
946
+ }
947
+ }
948
+ return ids;
949
+ }
663
950
 
664
951
  // src/convert.ts
665
952
  function convert(conversation, route, options = {}) {
@@ -693,6 +980,42 @@ function fromCanonical(canonical, to, options) {
693
980
 
694
981
  // src/response.ts
695
982
  var num = (value) => typeof value === "number" ? value : 0;
983
+ var nonEmptyString3 = (value) => typeof value === "string" && value.length > 0;
984
+ function responseRoot(body, reporter, provider) {
985
+ if (isRecord(body)) return body;
986
+ reporter.warn("dropped-content", `Dropped malformed ${provider} response body; expected an object.`);
987
+ return {};
988
+ }
989
+ function responseArrayField(root, field, reporter, context, noun) {
990
+ const value = root[field];
991
+ if (value === void 0) {
992
+ reporter.warn("dropped-content", `${context} missing ${field} array; no ${noun} were read.`);
993
+ return [];
994
+ }
995
+ if (!Array.isArray(value)) {
996
+ reporter.warn("dropped-content", `Dropped malformed ${context} ${field}; expected an array.`);
997
+ return [];
998
+ }
999
+ return value;
1000
+ }
1001
+ function firstResponseRecord(items, reporter, provider, noun) {
1002
+ if (items.length === 0) return {};
1003
+ if (isRecord(items[0])) return items[0];
1004
+ reporter.warn("dropped-content", `Dropped malformed ${provider} response ${noun}; expected an object.`);
1005
+ return {};
1006
+ }
1007
+ function responseRecordField(root, field, reporter, context, noun) {
1008
+ const value = root[field];
1009
+ if (value === void 0) {
1010
+ reporter.warn("dropped-content", `${context} missing ${field} object; no ${noun} were read.`);
1011
+ return {};
1012
+ }
1013
+ if (!isRecord(value)) {
1014
+ reporter.warn("dropped-content", `Dropped malformed ${context} ${field}; expected an object.`);
1015
+ return {};
1016
+ }
1017
+ return value;
1018
+ }
696
1019
  function buildMessage(text, toolCalls) {
697
1020
  const message = { role: "assistant", content: text ? text : null };
698
1021
  if (toolCalls.length > 0) message.tool_calls = toolCalls;
@@ -701,6 +1024,97 @@ function buildMessage(text, toolCalls) {
701
1024
  function finalReason(mapped, toolCalls) {
702
1025
  return toolCalls.length > 0 ? "tool_calls" : mapped;
703
1026
  }
1027
+ function normalizeProviderArguments(value, reporter, provider, part, fnName) {
1028
+ if (typeof value !== "string") return stringifyArgumentsObject(value, reporter, provider, part, fnName);
1029
+ const parsed = tryParseJson(value);
1030
+ if (parsed.ok && isRecord(parsed.value)) return value;
1031
+ reporter.warn(
1032
+ "invalid-json-arguments",
1033
+ `${provider} ${part} '${fnName}' had arguments that were not a JSON object string; used an empty object instead.`
1034
+ );
1035
+ return "{}";
1036
+ }
1037
+ function normalizeOpenAIToolCalls(value, reporter) {
1038
+ if (value === void 0) return [];
1039
+ if (!Array.isArray(value)) {
1040
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Chat Completions tool_calls; expected an array.");
1041
+ return [];
1042
+ }
1043
+ const toolCalls = [];
1044
+ const ids = createToolCallIdGenerator(
1045
+ value.flatMap(
1046
+ (call) => isRecord(call) && (call.type === void 0 || call.type === "function") && nonEmptyString3(call.id) ? [call.id] : []
1047
+ )
1048
+ );
1049
+ for (const call of value) {
1050
+ if (!isRecord(call)) {
1051
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Chat Completions tool_call; expected an object.");
1052
+ continue;
1053
+ }
1054
+ if (call.type !== void 0 && call.type !== "function") {
1055
+ reporter.warn(
1056
+ "dropped-content",
1057
+ `OpenAI Chat Completions tool_call type '${String(call.type)}' is not supported; dropped.`
1058
+ );
1059
+ continue;
1060
+ }
1061
+ const fn = isRecord(call.function) ? call.function : {};
1062
+ const name = providerFunctionName(fn.name, reporter, "OpenAI Chat Completions", "tool_call.function");
1063
+ const providedId = nonEmptyString3(call.id) ? call.id : void 0;
1064
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
1065
+ if (!providedId) {
1066
+ reporter.warn("generated-id", `OpenAI Chat Completions tool_call '${name}' had no id; generated '${id}'.`);
1067
+ } else if (id !== providedId) {
1068
+ reporter.warn(
1069
+ "generated-id",
1070
+ `OpenAI Chat Completions tool_call '${name}' reused id '${providedId}'; generated '${id}'.`
1071
+ );
1072
+ }
1073
+ const args = normalizeProviderArguments(
1074
+ fn.arguments,
1075
+ reporter,
1076
+ "OpenAI Chat Completions",
1077
+ "tool_call.function",
1078
+ name
1079
+ );
1080
+ toolCalls.push({ id, type: "function", function: { name, arguments: args } });
1081
+ }
1082
+ return toolCalls;
1083
+ }
1084
+ function normalizeOpenAIFunctionCall(value, reporter) {
1085
+ if (!isRecord(value)) return [];
1086
+ const name = providerFunctionName(value.name, reporter, "OpenAI Chat Completions", "function_call");
1087
+ const id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_0`;
1088
+ reporter.warn("generated-id", `OpenAI Chat Completions function_call '${name}' had no id; generated '${id}'.`);
1089
+ const args = normalizeProviderArguments(value.arguments, reporter, "OpenAI Chat Completions", "function_call", name);
1090
+ return [{ id, type: "function", function: { name, arguments: args } }];
1091
+ }
1092
+ function openAIChatContentText(content, reporter) {
1093
+ if (typeof content === "string") return content;
1094
+ if (content === null || content === void 0) return "";
1095
+ if (!Array.isArray(content)) {
1096
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Chat Completions response content.");
1097
+ return "";
1098
+ }
1099
+ const pieces = [];
1100
+ for (const part of content) {
1101
+ if (typeof part === "string") {
1102
+ pieces.push(part);
1103
+ } else if (isRecord(part) && typeof part.text === "string") {
1104
+ pieces.push(part.text);
1105
+ } else if (isRecord(part) && part.type === "refusal" && typeof part.refusal === "string") {
1106
+ pieces.push(part.refusal);
1107
+ } else {
1108
+ reporter.warn("dropped-content", "Dropped unsupported OpenAI Chat Completions response content part.");
1109
+ }
1110
+ }
1111
+ return pieces.join("");
1112
+ }
1113
+ function openAIChatMessageText(message, reporter) {
1114
+ const content = openAIChatContentText(message.content, reporter);
1115
+ if (content) return content;
1116
+ return typeof message.refusal === "string" ? message.refusal : content;
1117
+ }
704
1118
  var OPENAI_FINISH = {
705
1119
  stop: "stop",
706
1120
  length: "length",
@@ -708,16 +1122,27 @@ var OPENAI_FINISH = {
708
1122
  content_filter: "content_filter",
709
1123
  function_call: "tool_calls"
710
1124
  };
711
- function responseFromOpenAI(body) {
712
- const root = isRecord(body) ? body : {};
713
- const choice = Array.isArray(root.choices) && isRecord(root.choices[0]) ? root.choices[0] : {};
714
- const message = isRecord(choice.message) ? choice.message : {};
715
- const text = typeof message.content === "string" ? message.content : textOf(message.content);
716
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
1125
+ function responseFromOpenAI(body, options = {}) {
1126
+ const reporter = new Reporter(options);
1127
+ const isObjectBody = isRecord(body);
1128
+ const root = isObjectBody ? body : responseRoot(body, reporter, "OpenAI Chat Completions");
1129
+ const choices = isObjectBody ? responseArrayField(root, "choices", reporter, "OpenAI Chat Completions response", "choices") : [];
1130
+ const hasChoiceRecord = choices.length > 0 && isRecord(choices[0]);
1131
+ const choice = firstResponseRecord(choices, reporter, "OpenAI Chat Completions", "choice");
1132
+ const message = hasChoiceRecord ? responseRecordField(
1133
+ choice,
1134
+ "message",
1135
+ reporter,
1136
+ "OpenAI Chat Completions response choice",
1137
+ "message content or tool calls"
1138
+ ) : {};
1139
+ const text = openAIChatMessageText(message, reporter);
1140
+ const toolCalls = normalizeOpenAIToolCalls(message.tool_calls, reporter);
1141
+ const normalizedToolCalls = toolCalls.length > 0 ? toolCalls : normalizeOpenAIFunctionCall(message.function_call, reporter);
717
1142
  const usage = isRecord(root.usage) ? root.usage : {};
718
1143
  return {
719
- message: buildMessage(text, toolCalls),
720
- finishReason: finalReason(OPENAI_FINISH[String(choice.finish_reason)] ?? "unknown", toolCalls),
1144
+ message: buildMessage(text, normalizedToolCalls),
1145
+ finishReason: finalReason(OPENAI_FINISH[String(choice.finish_reason)] ?? "unknown", normalizedToolCalls),
721
1146
  usage: { inputTokens: num(usage.prompt_tokens), outputTokens: num(usage.completion_tokens) }
722
1147
  };
723
1148
  }
@@ -725,35 +1150,85 @@ var OPENAI_RESPONSES_INCOMPLETE = {
725
1150
  max_output_tokens: "length",
726
1151
  content_filter: "content_filter"
727
1152
  };
1153
+ function openAIResponsesContentText(content, reporter) {
1154
+ if (content === void 0) return "";
1155
+ if (!Array.isArray(content)) {
1156
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses message content.");
1157
+ return "";
1158
+ }
1159
+ const pieces = [];
1160
+ for (const part of content) {
1161
+ if (!isRecord(part)) {
1162
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses message content part.");
1163
+ } else if (typeof part.text === "string" && (part.type === "output_text" || part.type === "text")) {
1164
+ pieces.push(part.text);
1165
+ } else if (part.type === "refusal" && typeof part.refusal === "string") {
1166
+ pieces.push(part.refusal);
1167
+ } else {
1168
+ reporter.warn("dropped-content", "Dropped unsupported OpenAI Responses message content part.");
1169
+ }
1170
+ }
1171
+ return pieces.join("");
1172
+ }
728
1173
  function responseApiFinishReason(root) {
729
1174
  if (root.status === "completed") return "stop";
730
1175
  if (root.status !== "incomplete") return "unknown";
731
1176
  const details = isRecord(root.incomplete_details) ? root.incomplete_details : {};
732
1177
  return OPENAI_RESPONSES_INCOMPLETE[String(details.reason)] ?? "unknown";
733
1178
  }
734
- function responseFromOpenAIResponses(body) {
735
- const root = isRecord(body) ? body : {};
736
- const output = Array.isArray(root.output) ? root.output : [];
1179
+ function openAIResponsesOutput(root, reporter) {
1180
+ if (root.output === void 0) {
1181
+ reporter.warn(
1182
+ "dropped-content",
1183
+ "OpenAI Responses response missing top-level output array; no output items were read."
1184
+ );
1185
+ return [];
1186
+ }
1187
+ if (!Array.isArray(root.output)) {
1188
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses top-level output; expected an array.");
1189
+ return [];
1190
+ }
1191
+ return root.output;
1192
+ }
1193
+ function responseFromOpenAIResponses(body, options = {}) {
1194
+ const reporter = new Reporter(options);
1195
+ const isObjectBody = isRecord(body);
1196
+ const root = isObjectBody ? body : responseRoot(body, reporter, "OpenAI Responses");
1197
+ const output = isObjectBody ? openAIResponsesOutput(root, reporter) : [];
737
1198
  const textPieces = [];
738
1199
  const toolCalls = [];
739
- let counter = 0;
1200
+ const ids = createToolCallIdGenerator(
1201
+ output.flatMap((item) => {
1202
+ if (!isRecord(item) || item.type !== "function_call") return [];
1203
+ if (nonEmptyString3(item.call_id)) return [item.call_id];
1204
+ return nonEmptyString3(item.id) ? [item.id] : [];
1205
+ })
1206
+ );
740
1207
  for (const item of output) {
741
- if (!isRecord(item)) continue;
1208
+ if (!isRecord(item)) {
1209
+ reporter.warn("dropped-content", "Dropped malformed OpenAI Responses output item.");
1210
+ continue;
1211
+ }
742
1212
  if (item.type === "message") {
743
- const content = Array.isArray(item.content) ? item.content : [];
744
- for (const part of content) {
745
- if (!isRecord(part)) continue;
746
- if (typeof part.text === "string" && (part.type === "output_text" || part.type === "text")) {
747
- textPieces.push(part.text);
748
- } else if (part.type === "refusal" && typeof part.refusal === "string") {
749
- textPieces.push(part.refusal);
750
- }
1213
+ textPieces.push(openAIResponsesContentText(item.content, reporter));
1214
+ } else if (item.type === "function_call") {
1215
+ const name = providerFunctionName(item.name, reporter, "OpenAI Responses", "function_call");
1216
+ const callId = nonEmptyString3(item.call_id) ? item.call_id : void 0;
1217
+ const itemId = nonEmptyString3(item.id) ? item.id : void 0;
1218
+ const id = callId ?? itemId;
1219
+ const toolCallId = id ? ids.claim(id, name) : ids.generate(name);
1220
+ if (!callId && !itemId) {
1221
+ reporter.warn("generated-id", `OpenAI Responses function_call '${name}' had no id; generated '${toolCallId}'.`);
1222
+ } else if (toolCallId !== id) {
1223
+ reporter.warn(
1224
+ "generated-id",
1225
+ `OpenAI Responses function_call '${name}' reused id '${id}'; generated '${toolCallId}'.`
1226
+ );
751
1227
  }
752
- } else if (item.type === "function_call" && typeof item.name === "string") {
753
- const name = item.name;
754
- 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++}`;
755
- const args = typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {});
756
- toolCalls.push({ id, type: "function", function: { name, arguments: args } });
1228
+ const args = normalizeProviderArguments(item.arguments, reporter, "OpenAI Responses", "function_call", name);
1229
+ toolCalls.push({ id: toolCallId, type: "function", function: { name, arguments: args } });
1230
+ } else {
1231
+ reporter.warn("dropped-content", `Dropped unsupported OpenAI Responses output item '${String(item.type)}'.`);
757
1232
  }
758
1233
  }
759
1234
  const usage = isRecord(root.usage) ? root.usage : {};
@@ -771,21 +1246,47 @@ var ANTHROPIC_FINISH = {
771
1246
  refusal: "content_filter",
772
1247
  pause_turn: "unknown"
773
1248
  };
774
- function responseFromAnthropic(body) {
775
- const root = isRecord(body) ? body : {};
776
- const blocks = Array.isArray(root.content) ? root.content : [];
1249
+ function responseFromAnthropic(body, options = {}) {
1250
+ const reporter = new Reporter(options);
1251
+ const isObjectBody = isRecord(body);
1252
+ const root = isObjectBody ? body : responseRoot(body, reporter, "Anthropic");
1253
+ const blocks = isObjectBody ? responseArrayField(root, "content", reporter, "Anthropic response", "content blocks") : [];
777
1254
  const textPieces = [];
778
1255
  const toolCalls = [];
1256
+ const ids = createToolCallIdGenerator(
1257
+ blocks.flatMap(
1258
+ (block) => isRecord(block) && block.type === "tool_use" && nonEmptyString3(block.id) ? [block.id] : []
1259
+ )
1260
+ );
779
1261
  for (const block of blocks) {
780
- if (!isRecord(block)) continue;
1262
+ if (!isRecord(block)) {
1263
+ reporter.warn("dropped-content", "Dropped a malformed Anthropic response content block.");
1264
+ continue;
1265
+ }
781
1266
  if (block.type === "text" && typeof block.text === "string") {
782
1267
  textPieces.push(block.text);
783
- } else if (block.type === "tool_use" && typeof block.name === "string") {
1268
+ } else if (block.type === "tool_use") {
1269
+ const name = providerFunctionName(block.name, reporter, "Anthropic", "tool_use");
1270
+ const providedId = nonEmptyString3(block.id) ? block.id : void 0;
1271
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
1272
+ if (!providedId) {
1273
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' had no id; generated '${id}'.`);
1274
+ } else if (id !== providedId) {
1275
+ reporter.warn("generated-id", `Anthropic tool_use '${name}' reused id '${providedId}'; generated '${id}'.`);
1276
+ }
784
1277
  toolCalls.push({
785
- id: typeof block.id === "string" ? block.id : "",
1278
+ id,
786
1279
  type: "function",
787
- function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) }
1280
+ function: {
1281
+ name,
1282
+ arguments: stringifyArgumentsObject(block.input, reporter, "Anthropic", "tool_use", name)
1283
+ }
788
1284
  });
1285
+ } else {
1286
+ reporter.warn(
1287
+ "dropped-content",
1288
+ `Anthropic response content block '${String(block.type)}' is not supported; dropped.`
1289
+ );
789
1290
  }
790
1291
  }
791
1292
  const usage = isRecord(root.usage) ? root.usage : {};
@@ -800,30 +1301,63 @@ var GEMINI_FINISH = {
800
1301
  MAX_TOKENS: "length",
801
1302
  SAFETY: "content_filter",
802
1303
  RECITATION: "content_filter",
803
- MALFORMED_FUNCTION_CALL: "content_filter"
1304
+ BLOCKLIST: "content_filter",
1305
+ PROHIBITED_CONTENT: "content_filter",
1306
+ SPII: "content_filter",
1307
+ MALFORMED_FUNCTION_CALL: "content_filter",
1308
+ MODEL_ARMOR: "content_filter",
1309
+ IMAGE_SAFETY: "content_filter",
1310
+ IMAGE_PROHIBITED_CONTENT: "content_filter",
1311
+ IMAGE_RECITATION: "content_filter"
804
1312
  };
805
1313
  function responseFromGemini(body, options = {}) {
806
1314
  const reporter = new Reporter(options);
807
- const root = isRecord(body) ? body : {};
808
- const candidate = Array.isArray(root.candidates) && isRecord(root.candidates[0]) ? root.candidates[0] : {};
1315
+ const isObjectBody = isRecord(body);
1316
+ const root = isObjectBody ? body : responseRoot(body, reporter, "Gemini");
1317
+ const candidates = isObjectBody ? responseArrayField(root, "candidates", reporter, "Gemini response", "candidates") : [];
1318
+ const hasCandidateRecord = candidates.length > 0 && isRecord(candidates[0]);
1319
+ const candidate = firstResponseRecord(candidates, reporter, "Gemini", "candidate");
809
1320
  const content = isRecord(candidate.content) ? candidate.content : {};
810
- const parts = Array.isArray(content.parts) ? content.parts : [];
1321
+ if (hasCandidateRecord && !isRecord(candidate.content)) {
1322
+ reporter.warn("dropped-content", "Dropped malformed Gemini response candidate content; expected an object.");
1323
+ }
1324
+ const parts = hasCandidateRecord && isRecord(candidate.content) ? responseArrayField(content, "parts", reporter, "Gemini response candidate content", "content parts") : [];
811
1325
  const textPieces = [];
812
1326
  const toolCalls = [];
813
- let counter = 0;
1327
+ const ids = createToolCallIdGenerator(
1328
+ parts.flatMap(
1329
+ (part) => isRecord(part) && isRecord(part.functionCall) && nonEmptyString3(part.functionCall.id) ? [part.functionCall.id] : []
1330
+ )
1331
+ );
814
1332
  for (const part of parts) {
815
- if (!isRecord(part)) continue;
1333
+ if (!isRecord(part)) {
1334
+ reporter.warn("dropped-content", "Dropped a malformed Gemini response content part.");
1335
+ continue;
1336
+ }
816
1337
  if (isRecord(part.functionCall)) {
817
1338
  const call = part.functionCall;
818
- const name = typeof call.name === "string" ? call.name : "function";
819
- let id = call.id;
820
- if (!id) {
821
- id = `call_${name.replace(/[^a-zA-Z0-9_-]/g, "_")}_${counter++}`;
1339
+ const name = providerFunctionName(call.name, reporter, "Gemini", "functionCall");
1340
+ const providedId = nonEmptyString3(call.id) ? call.id : void 0;
1341
+ const id = providedId ? ids.claim(providedId, name) : ids.generate(name);
1342
+ if (!providedId) {
822
1343
  reporter.warn("generated-id", `Gemini functionCall '${name}' had no id; generated '${id}'.`);
1344
+ } else if (id !== providedId) {
1345
+ reporter.warn("generated-id", `Gemini functionCall '${name}' reused id '${providedId}'; generated '${id}'.`);
823
1346
  }
824
- toolCalls.push({ id, type: "function", function: { name, arguments: JSON.stringify(call.args ?? {}) } });
1347
+ toolCalls.push({
1348
+ id,
1349
+ type: "function",
1350
+ function: {
1351
+ name,
1352
+ arguments: stringifyArgumentsObject(call.args, reporter, "Gemini", "functionCall", name)
1353
+ }
1354
+ });
1355
+ } else if (part.thought === true) {
1356
+ reporter.warn("dropped-content", "Dropped a Gemini thought (reasoning) response part.");
825
1357
  } else if (typeof part.text === "string") {
826
1358
  textPieces.push(part.text);
1359
+ } else {
1360
+ reporter.warn("dropped-content", "Dropped an unsupported Gemini response content part.");
827
1361
  }
828
1362
  }
829
1363
  const usage = isRecord(root.usageMetadata) ? root.usageMetadata : {};
@@ -836,17 +1370,31 @@ function responseFromGemini(body, options = {}) {
836
1370
  function normalizeResponse(body, route, options = {}) {
837
1371
  switch (route.from) {
838
1372
  case "openai":
839
- return responseFromOpenAI(body);
1373
+ return responseFromOpenAI(body, options);
840
1374
  case "openai-responses":
841
- return responseFromOpenAIResponses(body);
1375
+ return responseFromOpenAIResponses(body, options);
842
1376
  case "anthropic":
843
- return responseFromAnthropic(body);
1377
+ return responseFromAnthropic(body, options);
844
1378
  case "gemini":
845
1379
  return responseFromGemini(body, options);
846
1380
  default:
847
1381
  throw new Error(`Unknown source provider: ${String(route.from)}`);
848
1382
  }
849
1383
  }
1384
+
1385
+ // src/types.ts
1386
+ var warningCodes = Object.freeze([
1387
+ "generated-id",
1388
+ "unmapped-tool-result",
1389
+ "merged-role",
1390
+ "dropped-content",
1391
+ "dropped-metadata",
1392
+ "invalid-json-arguments",
1393
+ "system-midstream",
1394
+ "gemini-url-image",
1395
+ "gemini-url-media",
1396
+ "unsupported-modality"
1397
+ ]);
850
1398
  export {
851
1399
  convert,
852
1400
  fromAnthropic,
@@ -859,6 +1407,7 @@ export {
859
1407
  responseFromOpenAIResponses,
860
1408
  toAnthropic,
861
1409
  toDataUrl,
862
- toGemini
1410
+ toGemini,
1411
+ warningCodes
863
1412
  };
864
1413
  //# sourceMappingURL=index.js.map