@jsonstudio/llms 0.4.6 → 0.6.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.
Files changed (99) 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 +23 -17
  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 +135 -55
  49. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +80 -40
  50. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +5 -29
  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 +49 -38
  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 -10
  61. package/dist/conversion/shared/anthropic-message-utils.d.ts +9 -1
  62. package/dist/conversion/shared/anthropic-message-utils.js +414 -26
  63. package/dist/conversion/shared/bridge-actions.js +267 -95
  64. package/dist/conversion/shared/bridge-message-utils.js +54 -8
  65. package/dist/conversion/shared/bridge-policies.js +21 -2
  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 +109 -28
  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/protocol-state.d.ts +4 -0
  74. package/dist/conversion/shared/protocol-state.js +23 -0
  75. package/dist/conversion/shared/reasoning-normalizer.d.ts +1 -0
  76. package/dist/conversion/shared/reasoning-normalizer.js +50 -18
  77. package/dist/conversion/shared/responses-output-builder.d.ts +1 -1
  78. package/dist/conversion/shared/responses-output-builder.js +76 -25
  79. package/dist/conversion/shared/responses-reasoning-registry.d.ts +8 -0
  80. package/dist/conversion/shared/responses-reasoning-registry.js +61 -0
  81. package/dist/conversion/shared/responses-response-utils.js +32 -2
  82. package/dist/conversion/shared/responses-tool-utils.js +28 -2
  83. package/dist/conversion/shared/snapshot-hooks.d.ts +9 -0
  84. package/dist/conversion/shared/snapshot-hooks.js +60 -6
  85. package/dist/conversion/shared/snapshot-utils.d.ts +16 -0
  86. package/dist/conversion/shared/snapshot-utils.js +84 -0
  87. package/dist/conversion/shared/tool-filter-pipeline.js +46 -7
  88. package/dist/conversion/shared/tool-mapping.js +13 -2
  89. package/dist/filters/index.d.ts +18 -0
  90. package/dist/filters/index.js +0 -1
  91. package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +13 -0
  92. package/dist/filters/special/request-streaming-to-nonstreaming.js +13 -1
  93. package/dist/filters/special/request-tool-choice-policy.js +3 -1
  94. package/dist/filters/special/request-tool-list-filter.d.ts +11 -0
  95. package/dist/filters/special/request-tool-list-filter.js +20 -7
  96. package/dist/sse/shared/responses-output-normalizer.js +5 -4
  97. package/dist/sse/sse-to-json/builders/response-builder.js +24 -1
  98. package/dist/sse/types/responses-types.d.ts +2 -0
  99. 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;
443
513
  }
514
+ if (hasToolCalls) {
515
+ // Assistant-only tool call is valid; leave content empty to avoid fabricated summaries.
516
+ continue;
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];
@@ -652,58 +879,3 @@ const metadataProviderSentinelAction = (ctx) => {
652
879
  };
653
880
  registerBridgeAction('metadata.provider-field', metadataProviderFieldAction);
654
881
  registerBridgeAction('metadata.provider-sentinel', metadataProviderSentinelAction);
655
- const metadataSystemSentinelAction = (ctx) => {
656
- const sentinel = typeof ctx.descriptor.options?.sentinel === 'string'
657
- ? ctx.descriptor.options.sentinel
658
- : undefined;
659
- const targetKey = typeof ctx.descriptor.options?.target === 'string'
660
- ? ctx.descriptor.options.target
661
- : 'rawSystem';
662
- const stringifyOutbound = ctx.descriptor.options?.stringify !== false;
663
- if (!sentinel)
664
- return;
665
- const payload = ctx.stage.startsWith('request') ? ctx.state.rawRequest :
666
- ctx.stage.startsWith('response') ? ctx.state.rawResponse :
667
- undefined;
668
- if (!payload || typeof payload !== 'object')
669
- return;
670
- if (ctx.stage.endsWith('inbound')) {
671
- const rawValue = payload[sentinel];
672
- if (rawValue === undefined)
673
- return;
674
- let parsed = rawValue;
675
- if (typeof rawValue === 'string') {
676
- try {
677
- parsed = JSON.parse(rawValue);
678
- }
679
- catch {
680
- parsed = rawValue;
681
- }
682
- }
683
- delete payload[sentinel];
684
- const metadata = ensureMetadataRecord(ctx.state);
685
- metadata[targetKey] = parsed;
686
- return;
687
- }
688
- const metadata = ctx.state.metadata;
689
- if (!metadata || typeof metadata !== 'object') {
690
- delete payload[sentinel];
691
- return;
692
- }
693
- const value = metadata[targetKey];
694
- if (value === undefined) {
695
- delete payload[sentinel];
696
- return;
697
- }
698
- if (stringifyOutbound) {
699
- try {
700
- payload[sentinel] = JSON.stringify(value);
701
- }
702
- catch {
703
- payload[sentinel] = value;
704
- }
705
- return;
706
- }
707
- payload[sentinel] = value;
708
- };
709
- registerBridgeAction('metadata.system-sentinel', metadataSystemSentinelAction);
@@ -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,6 +132,7 @@ 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' } },
@@ -131,6 +141,7 @@ const RESPONSES_POLICY = {
131
141
  outbound: [
132
142
  { name: 'messages.normalize-history' },
133
143
  { name: 'tools.capture-results' },
144
+ toolCallNormalizationAction('responses_tool_call'),
134
145
  { name: 'tools.ensure-placeholders' },
135
146
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
136
147
  { name: 'messages.ensure-system-instruction' },
@@ -144,10 +155,12 @@ const RESPONSES_POLICY = {
144
155
  inbound: [
145
156
  { name: 'reasoning.attach-output' },
146
157
  reasoningAction('responses_reasoning'),
158
+ toolCallNormalizationAction('responses_tool_call'),
147
159
  { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
148
160
  ],
149
161
  outbound: [
150
162
  reasoningAction('responses_reasoning'),
163
+ toolCallNormalizationAction('responses_tool_call'),
151
164
  { name: 'metadata.extra-fields', options: { allowedKeys: RESPONSES_ALLOWED_FIELDS } }
152
165
  ]
153
166
  }
@@ -158,8 +171,8 @@ const OPENAI_CHAT_POLICY = {
158
171
  request: {
159
172
  inbound: [
160
173
  reasoningAction('openai_chat_reasoning'),
174
+ toolCallNormalizationAction('openai_chat_tool_call'),
161
175
  { name: 'messages.ensure-system-instruction' },
162
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } },
163
176
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } },
164
177
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
165
178
  { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
@@ -167,11 +180,11 @@ const OPENAI_CHAT_POLICY = {
167
180
  outbound: [
168
181
  { name: 'messages.normalize-history' },
169
182
  { name: 'tools.capture-results' },
183
+ toolCallNormalizationAction('openai_chat_tool_call'),
170
184
  { name: 'tools.ensure-placeholders' },
171
185
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
172
186
  { name: 'messages.ensure-system-instruction' },
173
187
  reasoningAction('openai_chat_reasoning'),
174
- { name: 'metadata.system-sentinel', options: { sentinel: '__rcc_raw_system', target: 'rawSystem' } },
175
188
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } },
176
189
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
177
190
  { name: 'metadata.provider-sentinel', options: { sentinel: '__rcc_provider_metadata', target: 'providerMetadata' } }
@@ -180,10 +193,12 @@ const OPENAI_CHAT_POLICY = {
180
193
  response: {
181
194
  inbound: [
182
195
  reasoningAction('openai_chat_reasoning'),
196
+ toolCallNormalizationAction('openai_chat_tool_call'),
183
197
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
184
198
  ],
185
199
  outbound: [
186
200
  reasoningAction('openai_chat_reasoning'),
201
+ toolCallNormalizationAction('openai_chat_tool_call'),
187
202
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
188
203
  ]
189
204
  }
@@ -194,12 +209,14 @@ const ANTHROPIC_POLICY = {
194
209
  request: {
195
210
  inbound: [
196
211
  reasoningAction('anthropic_reasoning'),
212
+ toolCallNormalizationAction('anthropic_tool_call'),
197
213
  { name: 'messages.ensure-system-instruction' },
198
214
  { name: 'metadata.extra-fields', options: { allowedKeys: ANTHROPIC_ALLOWED_FIELDS } }
199
215
  ],
200
216
  outbound: [
201
217
  { name: 'messages.normalize-history' },
202
218
  { name: 'tools.capture-results' },
219
+ toolCallNormalizationAction('anthropic_tool_call'),
203
220
  { name: 'tools.ensure-placeholders' },
204
221
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
205
222
  { name: 'messages.ensure-system-instruction' },
@@ -210,10 +227,12 @@ const ANTHROPIC_POLICY = {
210
227
  response: {
211
228
  inbound: [
212
229
  reasoningAction('anthropic_reasoning'),
230
+ toolCallNormalizationAction('anthropic_tool_call'),
213
231
  { name: 'metadata.extra-fields', options: { allowedKeys: ANTHROPIC_ALLOWED_FIELDS } }
214
232
  ],
215
233
  outbound: [
216
234
  reasoningAction('anthropic_reasoning'),
235
+ toolCallNormalizationAction('anthropic_tool_call'),
217
236
  { name: 'metadata.extra-fields', options: { allowedKeys: ANTHROPIC_ALLOWED_FIELDS } }
218
237
  ]
219
238
  }
@@ -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;