@jsonstudio/llms 0.4.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.js +28 -2
  2. package/dist/conversion/codecs/gemini-openai-codec.js +23 -0
  3. package/dist/conversion/codecs/responses-openai-codec.js +8 -1
  4. package/dist/conversion/hub/node-support.js +14 -1
  5. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +66 -0
  6. package/dist/conversion/hub/pipeline/hub-pipeline.js +284 -193
  7. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.d.ts +11 -0
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +6 -0
  9. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +16 -0
  10. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +17 -0
  11. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.d.ts +5 -0
  12. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js +17 -0
  13. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.d.ts +19 -0
  14. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +269 -0
  15. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.d.ts +18 -0
  16. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +141 -0
  17. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +11 -0
  18. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +29 -0
  19. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.d.ts +16 -0
  20. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +15 -0
  21. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.d.ts +17 -0
  22. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +18 -0
  23. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.d.ts +17 -0
  24. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +63 -0
  25. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.d.ts +11 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +6 -0
  27. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +12 -0
  28. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +6 -0
  29. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +13 -0
  30. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +43 -0
  31. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.d.ts +17 -0
  32. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +22 -0
  33. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.d.ts +16 -0
  34. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +19 -0
  35. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.d.ts +17 -0
  36. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +19 -0
  37. package/dist/conversion/hub/pipeline/stages/utils.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/stages/utils.js +11 -0
  39. package/dist/conversion/hub/pipeline/target-utils.d.ts +5 -0
  40. package/dist/conversion/hub/pipeline/target-utils.js +87 -0
  41. package/dist/conversion/hub/process/chat-process.js +11 -11
  42. package/dist/conversion/hub/response/provider-response.js +69 -122
  43. package/dist/conversion/hub/response/response-mappers.d.ts +19 -0
  44. package/dist/conversion/hub/response/response-mappers.js +22 -2
  45. package/dist/conversion/hub/response/response-runtime.d.ts +8 -0
  46. package/dist/conversion/hub/response/response-runtime.js +239 -6
  47. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +8 -0
  48. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +119 -59
  49. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +74 -13
  50. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +0 -9
  51. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +16 -13
  52. package/dist/conversion/hub/snapshot-recorder.d.ts +13 -0
  53. package/dist/conversion/hub/snapshot-recorder.js +90 -50
  54. package/dist/conversion/hub/standardized-bridge.js +44 -30
  55. package/dist/conversion/hub/types/chat-envelope.d.ts +68 -0
  56. package/dist/conversion/hub/types/standardized.d.ts +97 -0
  57. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +29 -2
  58. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +68 -1
  59. package/dist/conversion/responses/responses-openai-bridge.d.ts +6 -1
  60. package/dist/conversion/responses/responses-openai-bridge.js +132 -6
  61. package/dist/conversion/shared/anthropic-message-utils.d.ts +9 -1
  62. package/dist/conversion/shared/anthropic-message-utils.js +334 -14
  63. package/dist/conversion/shared/bridge-actions.js +267 -40
  64. package/dist/conversion/shared/bridge-message-utils.js +54 -8
  65. package/dist/conversion/shared/bridge-policies.js +29 -4
  66. package/dist/conversion/shared/chat-envelope-validator.d.ts +8 -0
  67. package/dist/conversion/shared/chat-envelope-validator.js +128 -0
  68. package/dist/conversion/shared/chat-request-filters.js +108 -25
  69. package/dist/conversion/shared/mcp-injection.js +41 -20
  70. package/dist/conversion/shared/openai-finalizer.d.ts +11 -0
  71. package/dist/conversion/shared/openai-finalizer.js +73 -0
  72. package/dist/conversion/shared/openai-message-normalize.js +32 -31
  73. package/dist/conversion/shared/reasoning-normalizer.d.ts +1 -0
  74. package/dist/conversion/shared/reasoning-normalizer.js +50 -18
  75. package/dist/conversion/shared/responses-output-builder.d.ts +1 -1
  76. package/dist/conversion/shared/responses-output-builder.js +76 -25
  77. package/dist/conversion/shared/responses-reasoning-registry.d.ts +8 -0
  78. package/dist/conversion/shared/responses-reasoning-registry.js +61 -0
  79. package/dist/conversion/shared/responses-response-utils.js +32 -2
  80. package/dist/conversion/shared/responses-tool-utils.js +28 -2
  81. package/dist/conversion/shared/snapshot-hooks.d.ts +9 -0
  82. package/dist/conversion/shared/snapshot-hooks.js +60 -6
  83. package/dist/conversion/shared/snapshot-utils.d.ts +16 -0
  84. package/dist/conversion/shared/snapshot-utils.js +84 -0
  85. package/dist/conversion/shared/tool-filter-pipeline.js +45 -5
  86. package/dist/conversion/shared/tool-governor.js +5 -0
  87. package/dist/conversion/shared/tool-mapping.js +13 -2
  88. package/dist/filters/special/request-tool-choice-policy.js +3 -1
  89. package/dist/filters/special/request-tool-list-filter.d.ts +11 -0
  90. package/dist/filters/special/request-tool-list-filter.js +20 -7
  91. package/dist/sse/shared/responses-output-normalizer.js +5 -4
  92. package/package.json +1 -1
@@ -184,22 +184,78 @@ function mapToolResults(state) {
184
184
  }
185
185
  return toolResults;
186
186
  }
187
- function hasToolResponse(messages, startIndex, callId) {
187
+ function findFirstNonToolIndex(messages, startIndex) {
188
+ let index = startIndex;
189
+ while (index < messages.length) {
190
+ const msg = messages[index];
191
+ if (!msg || typeof msg !== 'object') {
192
+ break;
193
+ }
194
+ const role = String(msg.role || '').toLowerCase();
195
+ if (role !== 'tool') {
196
+ break;
197
+ }
198
+ index++;
199
+ }
200
+ return index;
201
+ }
202
+ function extractToolCallIdentifier(message) {
203
+ const id = message.tool_call_id ||
204
+ message.call_id ||
205
+ message.tool_use_id ||
206
+ message.id;
207
+ return typeof id === 'string' && id.trim().length ? id.trim() : undefined;
208
+ }
209
+ function findToolResponseIndex(messages, startIndex, endIndex, callId) {
210
+ const limit = Math.min(endIndex, messages.length);
211
+ for (let i = startIndex; i < limit; i++) {
212
+ const msg = messages[i];
213
+ if (!msg || typeof msg !== 'object') {
214
+ continue;
215
+ }
216
+ const role = String(msg.role || '').toLowerCase();
217
+ if (role !== 'tool') {
218
+ break;
219
+ }
220
+ const id = extractToolCallIdentifier(msg);
221
+ if (id === callId) {
222
+ return i;
223
+ }
224
+ }
225
+ return -1;
226
+ }
227
+ function extractToolResponse(messages, startIndex, callId) {
188
228
  for (let i = startIndex; i < messages.length; i++) {
189
229
  const msg = messages[i];
190
- if (!msg || typeof msg !== 'object')
230
+ if (!msg || typeof msg !== 'object') {
191
231
  continue;
232
+ }
192
233
  const role = String(msg.role || '').toLowerCase();
193
- if (role !== 'tool')
234
+ if (role !== 'tool') {
194
235
  continue;
195
- const id = msg.tool_call_id ||
196
- msg.call_id ||
197
- msg.tool_use_id;
198
- if (typeof id === 'string' && id.trim() === callId) {
199
- return true;
200
236
  }
237
+ const id = extractToolCallIdentifier(msg);
238
+ if (id === callId) {
239
+ const [removed] = messages.splice(i, 1);
240
+ return removed;
241
+ }
242
+ }
243
+ return undefined;
244
+ }
245
+ function buildToolPlaceholder(callId, call, toolResults) {
246
+ const content = toolResults.get(callId) ?? '';
247
+ const inferredName = toolResults.get(`${callId}::name`) ||
248
+ normalizeToolName(call.name) ||
249
+ normalizeToolName(call?.function?.name);
250
+ const toolMessage = {
251
+ role: 'tool',
252
+ tool_call_id: callId,
253
+ content
254
+ };
255
+ if (inferredName) {
256
+ toolMessage.name = inferredName;
201
257
  }
202
- return false;
258
+ return toolMessage;
203
259
  }
204
260
  function normalizeToolName(value) {
205
261
  if (typeof value === 'string' && value.trim().length) {
@@ -211,18 +267,25 @@ const ensureToolResponsePlaceholders = (ctx) => {
211
267
  const messages = ensureMessagesArray(ctx.state);
212
268
  if (!messages.length)
213
269
  return;
214
- const inserts = [];
215
270
  const toolResults = mapToolResults(ctx.state);
216
- for (let i = 0; i < messages.length; i++) {
217
- const message = messages[i];
218
- if (!message || typeof message !== 'object')
271
+ let index = 0;
272
+ while (index < messages.length) {
273
+ const message = messages[index];
274
+ if (!message || typeof message !== 'object') {
275
+ index++;
219
276
  continue;
277
+ }
220
278
  const role = String(message.role || '').toLowerCase();
221
- if (role !== 'assistant')
279
+ if (role !== 'assistant') {
280
+ index++;
222
281
  continue;
282
+ }
223
283
  const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : undefined;
224
- if (!toolCalls?.length)
284
+ if (!toolCalls?.length) {
285
+ index++;
225
286
  continue;
287
+ }
288
+ let insertionIndex = index + 1;
226
289
  for (const call of toolCalls) {
227
290
  if (!call || typeof call !== 'object')
228
291
  continue;
@@ -231,30 +294,24 @@ const ensureToolResponsePlaceholders = (ctx) => {
231
294
  call.tool_call_id;
232
295
  if (typeof callId !== 'string' || !callId.trim())
233
296
  continue;
234
- if (hasToolResponse(messages, i + 1, callId))
297
+ const normalizedId = callId.trim();
298
+ const windowEnd = findFirstNonToolIndex(messages, insertionIndex);
299
+ const existingIndex = findToolResponseIndex(messages, insertionIndex, windowEnd, normalizedId);
300
+ if (existingIndex >= 0) {
301
+ insertionIndex = existingIndex + 1;
235
302
  continue;
236
- const content = toolResults.get(callId) ?? '';
237
- const inferredName = toolResults.get(`${callId}::name`) ||
238
- normalizeToolName(call.name) ||
239
- normalizeToolName(call?.function?.name);
240
- const toolMessage = {
241
- role: 'tool',
242
- tool_call_id: callId,
243
- content
244
- };
245
- if (inferredName) {
246
- toolMessage.name = inferredName;
247
303
  }
248
- inserts.push({ index: i + 1, message: toolMessage });
304
+ const relocated = extractToolResponse(messages, windowEnd, normalizedId);
305
+ if (relocated) {
306
+ messages.splice(insertionIndex, 0, relocated);
307
+ insertionIndex++;
308
+ continue;
309
+ }
310
+ const placeholder = buildToolPlaceholder(normalizedId, call, toolResults);
311
+ messages.splice(insertionIndex, 0, placeholder);
312
+ insertionIndex++;
249
313
  }
250
- }
251
- if (!inserts.length)
252
- return;
253
- let offset = 0;
254
- for (const entry of inserts) {
255
- const targetIndex = Math.min(messages.length, entry.index + offset);
256
- messages.splice(targetIndex, 0, entry.message);
257
- offset++;
314
+ index = Math.max(index + 1, insertionIndex);
258
315
  }
259
316
  };
260
317
  function assignReasoning(message, parts) {
@@ -434,13 +491,34 @@ const ensureOutputFieldsAction = (ctx) => {
434
491
  continue;
435
492
  }
436
493
  if (role === 'assistant') {
494
+ const toolCalls = Array.isArray(message.tool_calls)
495
+ ? message.tool_calls
496
+ : undefined;
497
+ const hasToolCalls = Boolean(toolCalls && toolCalls.length);
437
498
  const text = flattenContentToString(message.content);
438
- if (text === undefined) {
439
- message.content = '';
440
- }
441
- else if (typeof message.content !== 'string') {
499
+ const hasText = typeof text === 'string' && text.trim().length > 0;
500
+ if (text !== undefined && typeof message.content !== 'string') {
442
501
  message.content = text;
502
+ continue;
503
+ }
504
+ if (hasText) {
505
+ continue;
506
+ }
507
+ const reasoningText = typeof message.reasoning_content === 'string' && message.reasoning_content.trim().length
508
+ ? message.reasoning_content.trim()
509
+ : undefined;
510
+ if (reasoningText) {
511
+ message.content = reasoningText;
512
+ continue;
513
+ }
514
+ if (hasToolCalls) {
515
+ // Assistant-only tool call is valid; leave content empty to avoid fabricated summaries.
516
+ continue;
443
517
  }
518
+ const assistantFallback = typeof ctx.descriptor.options?.assistantFallback === 'string' && ctx.descriptor.options.assistantFallback.trim().length
519
+ ? ctx.descriptor.options.assistantFallback.trim()
520
+ : 'Assistant response unavailable.';
521
+ message.content = assistantFallback;
444
522
  }
445
523
  }
446
524
  };
@@ -478,6 +556,151 @@ registerBridgeAction('messages.ensure-output-fields', ensureOutputFieldsAction);
478
556
  registerBridgeAction('tools.ensure-placeholders', ensureToolPlaceholdersAction);
479
557
  registerBridgeAction('tools.capture-results', captureToolResultsAction);
480
558
  registerBridgeAction('reasoning.attach-output', attachReasoningOutputAction);
559
+ function deriveToolIdPrefix(ctx) {
560
+ if (typeof ctx.descriptor.options?.idPrefix === 'string' && ctx.descriptor.options.idPrefix.trim().length) {
561
+ return ctx.descriptor.options.idPrefix.trim();
562
+ }
563
+ if (typeof ctx.requestId === 'string' && ctx.requestId.trim().length) {
564
+ const safe = ctx.requestId.trim().replace(/[^A-Za-z0-9]/g, '');
565
+ if (safe.length) {
566
+ return `${safe.slice(-24)}_tool`;
567
+ }
568
+ }
569
+ const base = ctx.protocol || 'bridge';
570
+ return `${base}_tool`;
571
+ }
572
+ const normalizeToolIdentifiersAction = (ctx) => {
573
+ const messages = ensureMessagesArray(ctx.state);
574
+ const idPrefix = deriveToolIdPrefix(ctx);
575
+ let counter = 0;
576
+ const aliasMap = new Map();
577
+ const knownIds = new Set();
578
+ const pendingQueue = [];
579
+ const registerAlias = (raw, normalized) => {
580
+ if (typeof raw === 'string' && raw.trim().length) {
581
+ aliasMap.set(raw.trim(), normalized);
582
+ }
583
+ aliasMap.set(normalized, normalized);
584
+ };
585
+ const consumePending = (id) => {
586
+ const index = pendingQueue.indexOf(id);
587
+ if (index >= 0) {
588
+ pendingQueue.splice(index, 1);
589
+ }
590
+ };
591
+ const nextId = () => {
592
+ counter += 1;
593
+ return `${idPrefix}_${counter}`;
594
+ };
595
+ const normalizeIdValue = (raw, consumeQueue) => {
596
+ if (typeof raw === 'string' && raw.trim().length) {
597
+ const existing = raw.trim();
598
+ if (aliasMap.has(existing)) {
599
+ const normalized = aliasMap.get(existing);
600
+ knownIds.add(normalized);
601
+ return normalized;
602
+ }
603
+ if (consumeQueue && pendingQueue.length) {
604
+ const queued = pendingQueue.shift();
605
+ knownIds.add(queued);
606
+ aliasMap.set(existing, queued);
607
+ aliasMap.set(queued, queued);
608
+ return queued;
609
+ }
610
+ knownIds.add(existing);
611
+ aliasMap.set(existing, existing);
612
+ return existing;
613
+ }
614
+ if (consumeQueue && pendingQueue.length) {
615
+ const queued = pendingQueue.shift();
616
+ knownIds.add(queued);
617
+ aliasMap.set(queued, queued);
618
+ return queued;
619
+ }
620
+ const generated = nextId();
621
+ knownIds.add(generated);
622
+ aliasMap.set(generated, generated);
623
+ return generated;
624
+ };
625
+ const normalizeToolCall = (call, consumeQueue) => {
626
+ if (!call || typeof call !== 'object') {
627
+ return undefined;
628
+ }
629
+ const normalized = normalizeIdValue(call.id || call.tool_call_id || call.call_id, consumeQueue);
630
+ registerAlias(call.id, normalized);
631
+ registerAlias(call.tool_call_id, normalized);
632
+ registerAlias(call.call_id, normalized);
633
+ call.id = normalized;
634
+ call.tool_call_id = normalized;
635
+ call.call_id = normalized;
636
+ return normalized;
637
+ };
638
+ const normalizeToolOutputs = (outputs) => {
639
+ if (!Array.isArray(outputs)) {
640
+ return;
641
+ }
642
+ outputs.forEach((entry) => {
643
+ if (!entry || typeof entry !== 'object') {
644
+ return;
645
+ }
646
+ const normalized = normalizeIdValue(entry.tool_call_id || entry.call_id || entry.id, true);
647
+ registerAlias(entry.tool_call_id, normalized);
648
+ registerAlias(entry.call_id, normalized);
649
+ consumePending(normalized);
650
+ entry.tool_call_id = normalized;
651
+ entry.call_id = normalized;
652
+ });
653
+ };
654
+ for (const message of messages) {
655
+ if (!message || typeof message !== 'object')
656
+ continue;
657
+ const role = String(message.role || '').toLowerCase();
658
+ if (role === 'assistant') {
659
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : undefined;
660
+ if (!toolCalls?.length)
661
+ continue;
662
+ for (const call of toolCalls) {
663
+ const normalized = normalizeToolCall(call, false);
664
+ if (normalized) {
665
+ pendingQueue.push(normalized);
666
+ }
667
+ }
668
+ }
669
+ else if (role === 'tool') {
670
+ const normalized = normalizeIdValue(message.tool_call_id || message.call_id || message.id, true);
671
+ registerAlias(message.tool_call_id, normalized);
672
+ registerAlias(message.call_id, normalized);
673
+ consumePending(normalized);
674
+ message.tool_call_id = normalized;
675
+ message.call_id = normalized;
676
+ if (!message.id) {
677
+ message.id = normalized;
678
+ }
679
+ }
680
+ }
681
+ if (ctx.state.rawRequest && typeof ctx.state.rawRequest === 'object') {
682
+ normalizeToolOutputs(ctx.state.rawRequest.tool_outputs);
683
+ const required = ctx.state.rawRequest.required_action;
684
+ if (required && typeof required === 'object') {
685
+ const submit = required.submit_tool_outputs;
686
+ if (submit && typeof submit === 'object' && Array.isArray(submit.tool_calls)) {
687
+ const submitCalls = submit.tool_calls;
688
+ submitCalls.forEach((call) => normalizeToolCall(call, false));
689
+ }
690
+ }
691
+ }
692
+ if (ctx.state.capturedToolResults && Array.isArray(ctx.state.capturedToolResults)) {
693
+ ctx.state.capturedToolResults.forEach((entry) => {
694
+ const normalized = normalizeIdValue(entry?.tool_call_id ?? entry?.call_id, true);
695
+ registerAlias(entry?.tool_call_id, normalized);
696
+ registerAlias(entry?.call_id, normalized);
697
+ consumePending(normalized);
698
+ entry.tool_call_id = normalized;
699
+ entry.call_id = normalized;
700
+ });
701
+ }
702
+ };
703
+ registerBridgeAction('tools.normalize-call-ids', normalizeToolIdentifiersAction);
481
704
  const responsesOutputReasoningAction = (ctx) => {
482
705
  if (ctx.stage !== 'response_inbound')
483
706
  return;
@@ -633,6 +856,10 @@ const metadataProviderSentinelAction = (ctx) => {
633
856
  metadata[targetKey] = provider;
634
857
  return;
635
858
  }
859
+ if (ctx.stage === 'request_outbound') {
860
+ delete payload[sentinel];
861
+ return;
862
+ }
636
863
  const metadata = ctx.state.metadata;
637
864
  if (!metadata || typeof metadata !== 'object') {
638
865
  delete payload[sentinel];
@@ -1,5 +1,19 @@
1
1
  import { normalizeFunctionCallId, normalizeFunctionCallOutputId } from './bridge-id-utils.js';
2
2
  import { normalizeChatMessageContent } from './chat-output-normalizer.js';
3
+ function ensureAssistantToolCallIdentity(call, fallbackId) {
4
+ const resolved = (typeof call.id === 'string' && call.id.trim().length ? call.id.trim() : undefined) ??
5
+ (typeof call.tool_call_id === 'string' && call.tool_call_id.trim().length
6
+ ? call.tool_call_id.trim()
7
+ : undefined) ??
8
+ (typeof call.call_id === 'string' && call.call_id.trim().length
9
+ ? call.call_id.trim()
10
+ : undefined) ??
11
+ fallbackId;
12
+ call.id = resolved;
13
+ call.tool_call_id = resolved;
14
+ call.call_id = resolved;
15
+ return resolved;
16
+ }
3
17
  export function coerceBridgeRole(role) {
4
18
  if (typeof role === 'string') {
5
19
  const normalized = role.toLowerCase();
@@ -159,7 +173,6 @@ export function convertMessagesToBridgeInput(options) {
159
173
  if (typeof text === 'string') {
160
174
  const tRole = role === 'assistant' ? 'output_text' : 'input_text';
161
175
  const entry = {
162
- type: 'message',
163
176
  role,
164
177
  content: [{ type: tRole, text }]
165
178
  };
@@ -385,7 +398,18 @@ export function convertBridgeInputToChatMessages(options) {
385
398
  }
386
399
  const serialized = serializeToolArguments(parsedArgs, name, tools).trim();
387
400
  toolNameById.set(callId, name);
388
- messages.push({ role: 'assistant', tool_calls: [{ id: callId, type: 'function', function: { name, arguments: serialized } }] });
401
+ const toolCall = {
402
+ id: callId,
403
+ call_id: callId,
404
+ tool_call_id: callId,
405
+ type: 'function',
406
+ function: { name, arguments: serialized }
407
+ };
408
+ messages.push({
409
+ role: 'assistant',
410
+ content: '',
411
+ tool_calls: [toolCall]
412
+ });
389
413
  lastToolCallId = callId;
390
414
  continue;
391
415
  }
@@ -404,7 +428,12 @@ export function convertBridgeInputToChatMessages(options) {
404
428
  contentStr = fallbackText;
405
429
  }
406
430
  const nm = toolNameById.get(String(toolCallId));
407
- const toolMsg = { role: 'tool', tool_call_id: String(toolCallId), content: contentStr };
431
+ const toolMsg = {
432
+ role: 'tool',
433
+ tool_call_id: String(toolCallId),
434
+ id: String(toolCallId),
435
+ content: contentStr
436
+ };
408
437
  if (typeof nm === 'string' && nm.trim().length)
409
438
  toolMsg.name = nm;
410
439
  messages.push(toolMsg);
@@ -412,7 +441,12 @@ export function convertBridgeInputToChatMessages(options) {
412
441
  catch {
413
442
  const fallback = (output ?? fallbackText);
414
443
  const nm = toolNameById.get(String(toolCallId));
415
- const toolMsg = { role: 'tool', tool_call_id: String(toolCallId), content: String(fallback) };
444
+ const toolMsg = {
445
+ role: 'tool',
446
+ tool_call_id: String(toolCallId),
447
+ id: String(toolCallId),
448
+ content: String(fallback)
449
+ };
416
450
  if (typeof nm === 'string' && nm.trim().length)
417
451
  toolMsg.name = nm;
418
452
  messages.push(toolMsg);
@@ -425,8 +459,14 @@ export function convertBridgeInputToChatMessages(options) {
425
459
  if (entry && typeof entry.message === 'object' && Array.isArray(entry.message?.content)) {
426
460
  const explicit = entry.message;
427
461
  const nested = processMessageBlocks(Array.isArray(explicit.content) ? explicit.content : [], resolveFunctionName, tools, toolNameById, lastToolCallId, fallbackText);
428
- if (nested.toolCalls.length)
429
- messages.push({ role: 'assistant', tool_calls: nested.toolCalls });
462
+ if (nested.toolCalls.length) {
463
+ nested.toolCalls.forEach((call, idx) => ensureAssistantToolCallIdentity(call, `fc_call_${messages.length + idx + 1}`));
464
+ messages.push({
465
+ role: 'assistant',
466
+ content: '',
467
+ tool_calls: nested.toolCalls
468
+ });
469
+ }
430
470
  for (const msg of nested.toolMessages)
431
471
  messages.push(msg);
432
472
  const normalizedRole = coerceBridgeRole((explicit.role ?? entry.role) || 'user');
@@ -440,8 +480,14 @@ export function convertBridgeInputToChatMessages(options) {
440
480
  }
441
481
  if (!handledViaExplicitMessage) {
442
482
  const nested = processMessageBlocks(Array.isArray(entry.content) ? entry.content : [], resolveFunctionName, tools, toolNameById, lastToolCallId, fallbackText);
443
- if (nested.toolCalls.length)
444
- messages.push({ role: 'assistant', tool_calls: nested.toolCalls });
483
+ if (nested.toolCalls.length) {
484
+ nested.toolCalls.forEach((call, idx) => ensureAssistantToolCallIdentity(call, `fc_call_${messages.length + idx + 1}`));
485
+ messages.push({
486
+ role: 'assistant',
487
+ content: '',
488
+ tool_calls: nested.toolCalls
489
+ });
490
+ }
445
491
  for (const msg of nested.toolMessages)
446
492
  messages.push(msg);
447
493
  const normalizedRole = coerceBridgeRole(entry.role || 'user');
@@ -109,6 +109,15 @@ function reasoningAction(idPrefix) {
109
109
  }
110
110
  };
111
111
  }
112
+ function toolCallNormalizationAction(idPrefix) {
113
+ if (idPrefix && idPrefix.trim().length) {
114
+ return {
115
+ name: 'tools.normalize-call-ids',
116
+ options: { idPrefix: idPrefix.trim() }
117
+ };
118
+ }
119
+ return { name: 'tools.normalize-call-ids' };
120
+ }
112
121
  const RESPONSES_POLICY = {
113
122
  id: 'openai-responses-default',
114
123
  protocol: 'openai-responses',
@@ -123,32 +132,40 @@ const RESPONSES_POLICY = {
123
132
  },
124
133
  { name: 'messages.ensure-system-instruction' },
125
134
  reasoningAction('responses_reasoning'),
135
+ toolCallNormalizationAction('responses_tool_call'),
126
136
  { name: 'tools.ensure-placeholders' },
127
137
  { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
128
138
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
129
- { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
139
+ { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } },
140
+ { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
130
141
  ],
131
142
  outbound: [
132
143
  { name: 'messages.normalize-history' },
133
144
  { name: 'tools.capture-results' },
145
+ toolCallNormalizationAction('responses_tool_call'),
134
146
  { name: 'tools.ensure-placeholders' },
135
147
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
136
148
  { name: 'messages.ensure-system-instruction' },
137
149
  reasoningAction('responses_reasoning'),
138
150
  { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
139
151
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
140
- { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
152
+ { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } },
153
+ { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
141
154
  ]
142
155
  },
143
156
  response: {
144
157
  inbound: [
145
158
  { name: 'reasoning.attach-output' },
146
159
  reasoningAction('responses_reasoning'),
147
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
160
+ toolCallNormalizationAction('responses_tool_call'),
161
+ { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
162
+ { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
148
163
  ],
149
164
  outbound: [
150
165
  reasoningAction('responses_reasoning'),
151
- { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
166
+ toolCallNormalizationAction('responses_tool_call'),
167
+ { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } },
168
+ { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } }
152
169
  ]
153
170
  }
154
171
  };
@@ -158,6 +175,7 @@ const OPENAI_CHAT_POLICY = {
158
175
  request: {
159
176
  inbound: [
160
177
  reasoningAction('openai_chat_reasoning'),
178
+ toolCallNormalizationAction('openai_chat_tool_call'),
161
179
  { name: 'messages.ensure-system-instruction' },
162
180
  { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } },
163
181
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } },
@@ -167,6 +185,7 @@ const OPENAI_CHAT_POLICY = {
167
185
  outbound: [
168
186
  { name: 'messages.normalize-history' },
169
187
  { name: 'tools.capture-results' },
188
+ toolCallNormalizationAction('openai_chat_tool_call'),
170
189
  { name: 'tools.ensure-placeholders' },
171
190
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
172
191
  { name: 'messages.ensure-system-instruction' },
@@ -180,10 +199,12 @@ const OPENAI_CHAT_POLICY = {
180
199
  response: {
181
200
  inbound: [
182
201
  reasoningAction('openai_chat_reasoning'),
202
+ toolCallNormalizationAction('openai_chat_tool_call'),
183
203
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
184
204
  ],
185
205
  outbound: [
186
206
  reasoningAction('openai_chat_reasoning'),
207
+ toolCallNormalizationAction('openai_chat_tool_call'),
187
208
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
188
209
  ]
189
210
  }
@@ -194,12 +215,14 @@ const ANTHROPIC_POLICY = {
194
215
  request: {
195
216
  inbound: [
196
217
  reasoningAction('anthropic_reasoning'),
218
+ toolCallNormalizationAction('anthropic_tool_call'),
197
219
  { name: 'messages.ensure-system-instruction' },
198
220
  { name: 'metadata.extra-fields', options: { allowedKeys: ANTHROPIC_ALLOWED_FIELDS } }
199
221
  ],
200
222
  outbound: [
201
223
  { name: 'messages.normalize-history' },
202
224
  { name: 'tools.capture-results' },
225
+ toolCallNormalizationAction('anthropic_tool_call'),
203
226
  { name: 'tools.ensure-placeholders' },
204
227
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
205
228
  { name: 'messages.ensure-system-instruction' },
@@ -210,10 +233,12 @@ const ANTHROPIC_POLICY = {
210
233
  response: {
211
234
  inbound: [
212
235
  reasoningAction('anthropic_reasoning'),
236
+ toolCallNormalizationAction('anthropic_tool_call'),
213
237
  { name: 'metadata.extra-fields', options: { allowedKeys: ANTHROPIC_ALLOWED_FIELDS } }
214
238
  ],
215
239
  outbound: [
216
240
  reasoningAction('anthropic_reasoning'),
241
+ toolCallNormalizationAction('anthropic_tool_call'),
217
242
  { name: 'metadata.extra-fields', options: { allowedKeys: ANTHROPIC_ALLOWED_FIELDS } }
218
243
  ]
219
244
  }
@@ -0,0 +1,8 @@
1
+ import type { ChatEnvelope } from '../hub/types/chat-envelope.js';
2
+ export type ChatValidationStage = 'req_inbound' | 'req_outbound' | 'resp_inbound' | 'resp_outbound';
3
+ export interface ChatEnvelopeValidationOptions {
4
+ stage: ChatValidationStage;
5
+ direction: 'request' | 'response';
6
+ source?: string;
7
+ }
8
+ export declare function validateChatEnvelope(chat: ChatEnvelope, options: ChatEnvelopeValidationOptions): void;