@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
@@ -2,6 +2,8 @@ import { extractToolCallsFromReasoningText } from '../../shared/reasoning-tool-p
2
2
  import { deriveToolCallKey } from '../../shared/tool-call-utils.js';
3
3
  import { createBridgeActionState, runBridgeActionPipeline } from '../../shared/bridge-actions.js';
4
4
  import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-policies.js';
5
+ import { normalizeAnthropicToolName } from '../../shared/anthropic-message-utils.js';
6
+ import { registerResponsesReasoning, consumeResponsesReasoning, registerResponsesOutputTextMeta, consumeResponsesOutputTextMeta } from '../../shared/responses-reasoning-registry.js';
5
7
  function flattenAnthropicContent(content) {
6
8
  if (typeof content === 'string')
7
9
  return content;
@@ -19,12 +21,106 @@ function flattenAnthropicContent(content) {
19
21
  }
20
22
  return '';
21
23
  }
22
- export function buildOpenAIChatFromAnthropicMessage(payload) {
24
+ function createToolNameResolver(options) {
25
+ const reverse = new Map();
26
+ const aliasMap = options?.aliasMap;
27
+ if (aliasMap && typeof aliasMap === 'object') {
28
+ for (const [canonical, providerName] of Object.entries(aliasMap)) {
29
+ if (typeof canonical !== 'string' || typeof providerName !== 'string')
30
+ continue;
31
+ const normalizedProvider = providerName.trim().toLowerCase();
32
+ if (!normalizedProvider.length)
33
+ continue;
34
+ if (!reverse.has(normalizedProvider)) {
35
+ reverse.set(normalizedProvider, canonical.trim());
36
+ }
37
+ }
38
+ }
39
+ return (rawName) => {
40
+ const trimmed = typeof rawName === 'string' ? rawName.trim() : '';
41
+ if (!trimmed.length) {
42
+ return '';
43
+ }
44
+ const lookup = reverse.get(trimmed.toLowerCase());
45
+ if (lookup && lookup.trim().length) {
46
+ return lookup.trim();
47
+ }
48
+ const normalized = normalizeAnthropicToolName(trimmed);
49
+ return (normalized && normalized.trim().length ? normalized : trimmed).trim();
50
+ };
51
+ }
52
+ function extractAliasMapFromChatPayload(payload) {
53
+ const metadata = payload?.metadata;
54
+ if (metadata && typeof metadata === 'object' && metadata.anthropicToolNameMap) {
55
+ const candidate = metadata.anthropicToolNameMap;
56
+ if (candidate && typeof candidate === 'object') {
57
+ const serialized = {};
58
+ for (const [key, value] of Object.entries(candidate)) {
59
+ if (typeof key === 'string' && typeof value === 'string') {
60
+ const trimmedKey = key.trim();
61
+ const trimmedValue = value.trim();
62
+ if (trimmedKey.length && trimmedValue.length) {
63
+ serialized[trimmedKey] = trimmedValue;
64
+ }
65
+ }
66
+ }
67
+ if (Object.keys(serialized).length) {
68
+ return serialized;
69
+ }
70
+ }
71
+ }
72
+ if (payload?.anthropicToolNameMap && typeof payload.anthropicToolNameMap === 'object') {
73
+ const map = {};
74
+ for (const [key, value] of Object.entries(payload.anthropicToolNameMap)) {
75
+ if (typeof key === 'string' && typeof value === 'string') {
76
+ const trimmedKey = key.trim();
77
+ const trimmedValue = value.trim();
78
+ if (trimmedKey.length && trimmedValue.length) {
79
+ map[trimmedKey] = trimmedValue;
80
+ }
81
+ }
82
+ }
83
+ if (Object.keys(map).length) {
84
+ return map;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+ function createToolAliasSerializer(aliasMap) {
90
+ if (!aliasMap || typeof aliasMap !== 'object') {
91
+ return (name) => name;
92
+ }
93
+ const lookup = new Map();
94
+ for (const [canonical, providerName] of Object.entries(aliasMap)) {
95
+ if (typeof canonical !== 'string' || typeof providerName !== 'string')
96
+ continue;
97
+ const key = canonical.trim().toLowerCase();
98
+ if (!key.length)
99
+ continue;
100
+ if (!lookup.has(key)) {
101
+ lookup.set(key, providerName);
102
+ }
103
+ }
104
+ if (!lookup.size) {
105
+ return (name) => name;
106
+ }
107
+ return (name) => {
108
+ const trimmed = typeof name === 'string' ? name.trim() : '';
109
+ if (!trimmed.length) {
110
+ return name;
111
+ }
112
+ const resolved = lookup.get(trimmed.toLowerCase());
113
+ return resolved ? resolved : name;
114
+ };
115
+ }
116
+ export function buildOpenAIChatFromAnthropicMessage(payload, options) {
23
117
  const content = Array.isArray(payload?.content) ? payload.content : [];
24
118
  const textParts = [];
25
119
  const toolCalls = [];
120
+ const aliasCollector = {};
26
121
  const inferredToolCalls = [];
27
122
  const reasoningParts = [];
123
+ const resolveToolName = createToolNameResolver(options);
28
124
  if (typeof payload?.reasoning_content === 'string' && payload.reasoning_content.trim().length) {
29
125
  reasoningParts.push(String(payload.reasoning_content).trim());
30
126
  }
@@ -36,9 +132,10 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
36
132
  textParts.push(part.text);
37
133
  }
38
134
  else if (kind === 'tool_use') {
39
- const name = typeof part.name === 'string'
135
+ const rawName = typeof part.name === 'string'
40
136
  ? String(part.name)
41
137
  : '';
138
+ const name = rawName ? resolveToolName(rawName) : '';
42
139
  const id = typeof part.id === 'string'
43
140
  ? String(part.id)
44
141
  : `call_${Math.random().toString(36).slice(2, 10)}`;
@@ -57,6 +154,10 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
57
154
  }
58
155
  if (name) {
59
156
  toolCalls.push({ id, name, args });
157
+ const trimmedRaw = rawName.trim();
158
+ if (trimmedRaw.length && !aliasCollector[name]) {
159
+ aliasCollector[name] = trimmedRaw;
160
+ }
60
161
  }
61
162
  }
62
163
  else if (kind === 'thinking' || kind === 'reasoning') {
@@ -137,7 +238,7 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
137
238
  }
138
239
  const stopReason = typeof payload['stop_reason'] === 'string' ? payload['stop_reason'] : undefined;
139
240
  const finishReason = canonicalToolCalls.length ? 'tool_calls' : mapFinishReason(stopReason);
140
- return {
241
+ const chatResponse = {
141
242
  id: typeof payload.id === 'string' ? payload.id : `chatcmpl_${Date.now()}`,
142
243
  object: 'chat.completion',
143
244
  created: typeof payload?.['created'] === 'number' ? payload['created'] : Math.floor(Date.now() / 1000),
@@ -153,10 +254,24 @@ export function buildOpenAIChatFromAnthropicMessage(payload) {
153
254
  ? payload['usage']
154
255
  : undefined
155
256
  };
257
+ const preserved = consumeResponsesReasoning(chatResponse.id);
258
+ if (preserved && preserved.length) {
259
+ chatResponse.__responses_reasoning = preserved;
260
+ }
261
+ const preservedOutputMeta = consumeResponsesOutputTextMeta(chatResponse.id);
262
+ if (preservedOutputMeta) {
263
+ chatResponse.__responses_output_text_meta = preservedOutputMeta;
264
+ }
265
+ if (Object.keys(aliasCollector).length && !chatResponse.anthropicToolNameMap) {
266
+ chatResponse.anthropicToolNameMap = aliasCollector;
267
+ }
268
+ return chatResponse;
156
269
  }
157
- export function buildAnthropicResponseFromChat(chatResponse) {
270
+ export function buildAnthropicResponseFromChat(chatResponse, options) {
158
271
  const choice = Array.isArray(chatResponse?.choices) ? chatResponse.choices[0] : undefined;
159
272
  const message = choice && typeof choice === 'object' ? choice.message : undefined;
273
+ const aliasMap = options?.aliasMap ?? extractAliasMapFromChatPayload(chatResponse);
274
+ const outboundAliasSerializer = createToolAliasSerializer(aliasMap);
160
275
  if (message) {
161
276
  try {
162
277
  const bridgePolicy = resolveBridgePolicy({ protocol: 'anthropic-messages' });
@@ -198,6 +313,7 @@ export function buildAnthropicResponseFromChat(chatResponse) {
198
313
  const fn = call.function || {};
199
314
  if (typeof fn?.name !== 'string')
200
315
  continue;
316
+ const serializedName = outboundAliasSerializer(fn.name);
201
317
  let parsedArgs = {};
202
318
  const args = fn.arguments;
203
319
  if (typeof args === 'string') {
@@ -214,10 +330,22 @@ export function buildAnthropicResponseFromChat(chatResponse) {
214
330
  contentBlocks.push({
215
331
  type: 'tool_use',
216
332
  id: typeof call.id === 'string' ? call.id : `call_${Math.random().toString(36).slice(2, 8)}`,
217
- name: fn.name,
333
+ name: serializedName,
218
334
  input: parsedArgs
219
335
  });
220
336
  }
337
+ const toolResults = extractToolResultBlocks(chatResponse);
338
+ for (const block of toolResults) {
339
+ const sanitized = {
340
+ type: 'tool_result',
341
+ tool_use_id: block.tool_use_id,
342
+ content: block.content ?? ''
343
+ };
344
+ if (typeof block.is_error === 'boolean') {
345
+ sanitized.is_error = block.is_error;
346
+ }
347
+ contentBlocks.push(sanitized);
348
+ }
221
349
  const usage = chatResponse?.usage;
222
350
  const stopReason = typeof choice?.finish_reason === 'string'
223
351
  ? choice.finish_reason
@@ -244,7 +372,14 @@ export function buildAnthropicResponseFromChat(chatResponse) {
244
372
  }
245
373
  : undefined
246
374
  };
247
- return sanitizeAnthropicMessage(raw);
375
+ const sanitized = sanitizeAnthropicMessage(raw);
376
+ if (Array.isArray(chatResponse?.__responses_reasoning)) {
377
+ registerResponsesReasoning(sanitized.id, chatResponse.__responses_reasoning);
378
+ }
379
+ if (chatResponse?.__responses_output_text_meta) {
380
+ registerResponsesOutputTextMeta(sanitized.id, chatResponse.__responses_output_text_meta);
381
+ }
382
+ return sanitized;
248
383
  }
249
384
  function sanitizeAnthropicMessage(message) {
250
385
  const sanitized = {};
@@ -276,6 +411,17 @@ function sanitizeContentBlock(block) {
276
411
  return null;
277
412
  return { type: 'text', text: block.text };
278
413
  }
414
+ if (type === 'thinking' || type === 'reasoning') {
415
+ const text = typeof block.text === 'string'
416
+ ? block.text
417
+ : flattenAnthropicContent(block.content);
418
+ if (!text || !String(text).trim().length)
419
+ return null;
420
+ return {
421
+ type: type === 'reasoning' ? 'reasoning' : 'thinking',
422
+ text: String(text)
423
+ };
424
+ }
279
425
  if (type === 'tool_use') {
280
426
  const id = typeof block.id === 'string' && block.id.trim() ? block.id : `call_${Math.random().toString(36).slice(2, 8)}`;
281
427
  const name = typeof block.name === 'string' ? block.name : '';
@@ -303,3 +449,90 @@ function sanitizeContentBlock(block) {
303
449
  }
304
450
  return null;
305
451
  }
452
+ function extractToolResultBlocks(chatResponse) {
453
+ const results = [];
454
+ const seen = new Set();
455
+ const append = (candidate) => {
456
+ if (!candidate)
457
+ return;
458
+ if (seen.has(candidate.tool_use_id)) {
459
+ return;
460
+ }
461
+ seen.add(candidate.tool_use_id);
462
+ results.push(candidate);
463
+ };
464
+ const primary = Array.isArray(chatResponse.tool_outputs)
465
+ ? chatResponse.tool_outputs
466
+ : [];
467
+ primary.forEach(entry => append(normalizeToolResultEntry(entry)));
468
+ const meta = chatResponse.metadata;
469
+ if (meta && typeof meta === 'object') {
470
+ const captured = meta.capturedToolResults;
471
+ if (Array.isArray(captured)) {
472
+ captured.forEach(entry => append(normalizeToolResultEntry(entry)));
473
+ }
474
+ }
475
+ if (choiceHasCapturedResults(chatResponse)) {
476
+ try {
477
+ const choice = Array.isArray(chatResponse?.choices) ? chatResponse.choices[0] : undefined;
478
+ const msgMeta = choice && typeof choice === 'object' ? choice.message : undefined;
479
+ const captured = msgMeta && typeof msgMeta === 'object'
480
+ ? msgMeta.capturedToolResults
481
+ : undefined;
482
+ if (Array.isArray(captured)) {
483
+ captured.forEach(entry => append(normalizeToolResultEntry(entry)));
484
+ }
485
+ }
486
+ catch {
487
+ /* ignore best-effort metadata extraction */
488
+ }
489
+ }
490
+ return results;
491
+ }
492
+ function choiceHasCapturedResults(chatResponse) {
493
+ if (!Array.isArray(chatResponse?.choices)) {
494
+ return false;
495
+ }
496
+ const first = chatResponse.choices[0];
497
+ if (!first || typeof first !== 'object') {
498
+ return false;
499
+ }
500
+ const message = first.message;
501
+ if (!message || typeof message !== 'object') {
502
+ return false;
503
+ }
504
+ return Array.isArray(message.capturedToolResults);
505
+ }
506
+ function normalizeToolResultEntry(entry) {
507
+ if (!entry || typeof entry !== 'object') {
508
+ return null;
509
+ }
510
+ const rawId = entry.tool_call_id ?? entry.call_id ?? entry.id;
511
+ if (typeof rawId !== 'string' || !rawId.trim().length) {
512
+ return null;
513
+ }
514
+ const toolUseId = rawId.trim();
515
+ const rawContent = 'content' in entry ? entry.content : entry.output;
516
+ const content = normalizeToolContent(rawContent);
517
+ const isError = typeof entry.is_error === 'boolean' ? entry.is_error : undefined;
518
+ return {
519
+ tool_use_id: toolUseId,
520
+ content,
521
+ is_error: isError
522
+ };
523
+ }
524
+ function normalizeToolContent(value) {
525
+ if (value == null) {
526
+ return undefined;
527
+ }
528
+ if (typeof value === 'string') {
529
+ return value;
530
+ }
531
+ try {
532
+ const serialized = JSON.stringify(value);
533
+ return serialized;
534
+ }
535
+ catch {
536
+ return String(value);
537
+ }
538
+ }
@@ -0,0 +1,8 @@
1
+ import type { SemanticMapper } from '../format-adapters/index.js';
2
+ import type { AdapterContext, ChatEnvelope } from '../types/chat-envelope.js';
3
+ import type { FormatEnvelope } from '../types/format-envelope.js';
4
+ export declare class AnthropicSemanticMapper implements SemanticMapper {
5
+ private readonly chatMapper;
6
+ toChat(format: FormatEnvelope, ctx: AdapterContext): Promise<ChatEnvelope>;
7
+ fromChat(chat: ChatEnvelope, ctx: AdapterContext): Promise<FormatEnvelope>;
8
+ }
@@ -3,7 +3,8 @@ import { buildOpenAIChatFromAnthropic, buildAnthropicRequestFromOpenAIChat } fro
3
3
  import { createBridgeActionState, runBridgeActionPipeline } from '../../shared/bridge-actions.js';
4
4
  import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-policies.js';
5
5
  import { encodeMetadataPassthrough, extractMetadataPassthrough } from '../../shared/metadata-passthrough.js';
6
- import { mapAnthropicToolsToChat } from '../../shared/anthropic-message-utils.js';
6
+ import { buildAnthropicToolAliasMap } from '../../shared/anthropic-message-utils.js';
7
+ import { ChatSemanticMapper } from './chat-mapper.js';
7
8
  const ANTHROPIC_PARAMETER_KEYS = [
8
9
  'model',
9
10
  'temperature',
@@ -32,6 +33,14 @@ const ANTHROPIC_TOP_LEVEL_FIELDS = new Set([
32
33
  ]);
33
34
  const PASSTHROUGH_METADATA_PREFIX = 'rcc_passthrough_';
34
35
  const PASSTHROUGH_PARAMETERS = ['tool_choice'];
36
+ function sanitizeAnthropicPayload(payload) {
37
+ for (const key of Object.keys(payload)) {
38
+ if (!ANTHROPIC_TOP_LEVEL_FIELDS.has(key)) {
39
+ delete payload[key];
40
+ }
41
+ }
42
+ return payload;
43
+ }
35
44
  function collectParameters(payload) {
36
45
  const params = {};
37
46
  for (const key of ANTHROPIC_PARAMETER_KEYS) {
@@ -44,39 +53,8 @@ function collectParameters(payload) {
44
53
  }
45
54
  return Object.keys(params).length ? params : undefined;
46
55
  }
47
- function normalizeToolContent(content) {
48
- if (typeof content === 'string')
49
- return content;
50
- if (content == null)
51
- return '';
52
- try {
53
- return JSON.stringify(content);
54
- }
55
- catch {
56
- return String(content ?? '');
57
- }
58
- }
59
- function collectToolOutputsFromMessages(messages, missing) {
60
- const outputs = [];
61
- messages.forEach((msg, index) => {
62
- if (!msg || typeof msg !== 'object')
63
- return;
64
- if (msg.role !== 'tool')
65
- return;
66
- const callId = msg.tool_call_id || msg.id;
67
- if (typeof callId !== 'string' || !callId.trim()) {
68
- missing.push({ path: `messages[${index}].tool_call_id`, reason: 'missing_tool_call_id' });
69
- return;
70
- }
71
- outputs.push({
72
- tool_call_id: callId.trim(),
73
- content: normalizeToolContent(msg.content),
74
- name: typeof msg.name === 'string' ? msg.name : undefined
75
- });
76
- });
77
- return outputs.length ? outputs : undefined;
78
- }
79
56
  export class AnthropicSemanticMapper {
57
+ chatMapper = new ChatSemanticMapper();
80
58
  async toChat(format, ctx) {
81
59
  const payload = (format.payload ?? {});
82
60
  const missing = [];
@@ -84,39 +62,109 @@ export class AnthropicSemanticMapper {
84
62
  missing.push({ path: 'messages', reason: 'absent' });
85
63
  if (typeof payload.model !== 'string')
86
64
  missing.push({ path: 'model', reason: 'absent' });
87
- const openaiPayload = buildOpenAIChatFromAnthropic(payload);
88
- const messages = Array.isArray(openaiPayload.messages)
89
- ? openaiPayload.messages
90
- : [];
91
- const toolOutputs = collectToolOutputsFromMessages(messages, missing);
92
- const tools = mapAnthropicToolsToChat(payload.tools, missing);
93
- let parameters = collectParameters(payload);
94
65
  const passthrough = extractMetadataPassthrough(payload.metadata, {
95
66
  prefix: PASSTHROUGH_METADATA_PREFIX,
96
67
  keys: PASSTHROUGH_PARAMETERS
97
68
  });
98
- if (passthrough.passthrough) {
99
- parameters = { ...(parameters || {}), ...passthrough.passthrough };
100
- }
101
- const metadata = { context: ctx };
69
+ const openaiPayload = buildOpenAIChatFromAnthropic(payload);
70
+ const canonicalContext = {
71
+ ...ctx,
72
+ providerProtocol: 'openai-chat',
73
+ entryEndpoint: ctx.entryEndpoint || '/v1/chat/completions'
74
+ };
75
+ const chatEnvelope = await this.chatMapper.toChat({
76
+ protocol: 'openai-chat',
77
+ direction: 'request',
78
+ payload: openaiPayload
79
+ }, canonicalContext);
80
+ const metadata = chatEnvelope.metadata ?? { context: canonicalContext };
81
+ chatEnvelope.metadata = metadata;
82
+ metadata.context = canonicalContext;
83
+ const resolveExtraFields = () => {
84
+ if (!isJsonObject(metadata.extraFields)) {
85
+ metadata.extraFields = {};
86
+ }
87
+ return metadata.extraFields;
88
+ };
102
89
  if (payload.tools && Array.isArray(payload.tools) && payload.tools.length === 0) {
103
90
  metadata.toolsFieldPresent = true;
91
+ resolveExtraFields().toolsFieldPresent = true;
92
+ }
93
+ const aliasMap = buildAnthropicToolAliasMap(payload.tools);
94
+ if (aliasMap) {
95
+ const extraFields = resolveExtraFields();
96
+ ctx.anthropicToolNameMap = aliasMap;
97
+ canonicalContext.anthropicToolNameMap = aliasMap;
98
+ metadata.anthropicToolNameMap = aliasMap;
99
+ extraFields.anthropicToolNameMap = aliasMap;
100
+ }
101
+ if (Array.isArray(payload.messages) && payload.messages.length) {
102
+ const shapes = payload.messages.map((entry) => {
103
+ if (!entry || typeof entry !== 'object') {
104
+ return 'unknown';
105
+ }
106
+ const rawContent = entry.content;
107
+ if (typeof rawContent === 'string') {
108
+ return 'string';
109
+ }
110
+ if (Array.isArray(rawContent)) {
111
+ return 'array';
112
+ }
113
+ if (rawContent === null || rawContent === undefined) {
114
+ return 'null';
115
+ }
116
+ return typeof rawContent;
117
+ });
118
+ const extraFields = resolveExtraFields();
119
+ const mirrorNode = extraFields.anthropicMirror && typeof extraFields.anthropicMirror === 'object'
120
+ ? extraFields.anthropicMirror
121
+ : {};
122
+ mirrorNode.messageContentShape = shapes;
123
+ extraFields.anthropicMirror = mirrorNode;
104
124
  }
105
125
  if (missing.length) {
106
- metadata.missingFields = missing;
126
+ metadata.missingFields = Array.isArray(metadata.missingFields)
127
+ ? [...metadata.missingFields, ...missing]
128
+ : missing;
107
129
  }
108
- if (passthrough.metadata) {
109
- metadata.providerMetadata = passthrough.metadata;
130
+ const providerMetadata = passthrough.metadata ??
131
+ (payload.metadata && isJsonObject(payload.metadata) ? jsonClone(payload.metadata) : undefined);
132
+ if (providerMetadata) {
133
+ metadata.providerMetadata = providerMetadata;
134
+ }
135
+ const mergedParameters = { ...(chatEnvelope.parameters ?? {}) };
136
+ const mergeParameters = (source) => {
137
+ if (!source) {
138
+ return;
139
+ }
140
+ for (const [key, value] of Object.entries(source)) {
141
+ if (mergedParameters[key] !== undefined) {
142
+ continue;
143
+ }
144
+ mergedParameters[key] = jsonClone(value);
145
+ }
146
+ };
147
+ mergeParameters(collectParameters(payload));
148
+ if (providerMetadata) {
149
+ mergedParameters.metadata = jsonClone(providerMetadata);
110
150
  }
111
- else if (payload.metadata && isJsonObject(payload.metadata)) {
112
- metadata.providerMetadata = jsonClone(payload.metadata);
151
+ if (passthrough.passthrough) {
152
+ for (const [key, value] of Object.entries(passthrough.passthrough)) {
153
+ mergedParameters[key] = jsonClone(value);
154
+ }
155
+ }
156
+ if (Object.keys(mergedParameters).length) {
157
+ chatEnvelope.parameters = mergedParameters;
158
+ }
159
+ else {
160
+ delete chatEnvelope.parameters;
113
161
  }
114
162
  try {
115
163
  const bridgePolicy = resolveBridgePolicy({ protocol: 'anthropic-messages' });
116
164
  const actions = resolvePolicyActions(bridgePolicy, 'request_inbound');
117
165
  if (actions?.length) {
118
166
  const actionState = createBridgeActionState({
119
- messages: messages,
167
+ messages: chatEnvelope.messages,
120
168
  rawRequest: payload,
121
169
  metadata: metadata
122
170
  });
@@ -133,13 +181,7 @@ export class AnthropicSemanticMapper {
133
181
  catch {
134
182
  // best-effort metadata extraction
135
183
  }
136
- return {
137
- messages,
138
- tools,
139
- toolOutputs,
140
- parameters,
141
- metadata
142
- };
184
+ return chatEnvelope;
143
185
  }
144
186
  async fromChat(chat, ctx) {
145
187
  const model = chat.parameters?.model;
@@ -147,11 +189,21 @@ export class AnthropicSemanticMapper {
147
189
  throw new Error('ChatEnvelope.parameters.model is required for anthropic-messages outbound conversion');
148
190
  }
149
191
  const baseRequest = {
150
- ...(chat.parameters || {}),
151
192
  model,
152
193
  messages: chat.messages,
153
194
  tools: chat.tools
154
195
  };
196
+ const trimmedParameters = chat.parameters && typeof chat.parameters === 'object' ? chat.parameters : undefined;
197
+ if (trimmedParameters) {
198
+ for (const [key, value] of Object.entries(trimmedParameters)) {
199
+ if (ANTHROPIC_TOP_LEVEL_FIELDS.has(key) || key === 'stop') {
200
+ if (key === 'messages' || key === 'tools') {
201
+ continue;
202
+ }
203
+ baseRequest[key] = value;
204
+ }
205
+ }
206
+ }
155
207
  const passthroughMetadata = encodeMetadataPassthrough(chat.parameters, {
156
208
  prefix: PASSTHROUGH_METADATA_PREFIX,
157
209
  keys: PASSTHROUGH_PARAMETERS
@@ -175,8 +227,15 @@ export class AnthropicSemanticMapper {
175
227
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
176
228
  baseRequest.tools = [];
177
229
  }
230
+ if (chat.metadata &&
231
+ typeof chat.metadata === 'object' &&
232
+ chat.metadata.extraFields &&
233
+ typeof chat.metadata.extraFields === 'object' &&
234
+ chat.metadata.extraFields.anthropicMirror) {
235
+ baseRequest.__anthropicMirror = jsonClone(chat.metadata.extraFields.anthropicMirror ?? {});
236
+ }
178
237
  const payloadSource = buildAnthropicRequestFromOpenAIChat(baseRequest);
179
- const payload = JSON.parse(JSON.stringify(payloadSource));
238
+ const payload = sanitizeAnthropicPayload(JSON.parse(JSON.stringify(payloadSource)));
180
239
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
181
240
  payload.tools = [];
182
241
  }
@@ -210,6 +269,7 @@ export class AnthropicSemanticMapper {
210
269
  catch {
211
270
  // ignore metadata propagation failures
212
271
  }
272
+ sanitizeAnthropicPayload(payload);
213
273
  return {
214
274
  protocol: 'anthropic-messages',
215
275
  direction: 'response',