@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
@@ -0,0 +1,8 @@
1
+ import type { JsonObject } from '../types/json.js';
2
+ type ToolAliasMap = Record<string, string>;
3
+ interface AnthropicResponseOptions {
4
+ aliasMap?: ToolAliasMap;
5
+ }
6
+ export declare function buildOpenAIChatFromAnthropicMessage(payload: JsonObject, options?: AnthropicResponseOptions): JsonObject;
7
+ export declare function buildAnthropicResponseFromChat(chatResponse: JsonObject, options?: AnthropicResponseOptions): JsonObject;
8
+ export {};
@@ -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,9 @@ 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';
8
+ import { ensureProtocolState, getProtocolState } from '../../shared/protocol-state.js';
7
9
  const ANTHROPIC_PARAMETER_KEYS = [
8
10
  'model',
9
11
  'temperature',
@@ -32,6 +34,14 @@ const ANTHROPIC_TOP_LEVEL_FIELDS = new Set([
32
34
  ]);
33
35
  const PASSTHROUGH_METADATA_PREFIX = 'rcc_passthrough_';
34
36
  const PASSTHROUGH_PARAMETERS = ['tool_choice'];
37
+ function sanitizeAnthropicPayload(payload) {
38
+ for (const key of Object.keys(payload)) {
39
+ if (!ANTHROPIC_TOP_LEVEL_FIELDS.has(key)) {
40
+ delete payload[key];
41
+ }
42
+ }
43
+ return payload;
44
+ }
35
45
  function collectParameters(payload) {
36
46
  const params = {};
37
47
  for (const key of ANTHROPIC_PARAMETER_KEYS) {
@@ -44,39 +54,18 @@ function collectParameters(payload) {
44
54
  }
45
55
  return Object.keys(params).length ? params : undefined;
46
56
  }
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);
57
+ function cloneAnthropicSystemBlocks(value) {
58
+ if (value === undefined || value === null) {
59
+ return undefined;
54
60
  }
55
- catch {
56
- return String(content ?? '');
61
+ const blocks = Array.isArray(value) ? value : [value];
62
+ if (!blocks.length) {
63
+ return undefined;
57
64
  }
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;
65
+ return blocks.map((entry) => jsonClone(entry));
78
66
  }
79
67
  export class AnthropicSemanticMapper {
68
+ chatMapper = new ChatSemanticMapper();
80
69
  async toChat(format, ctx) {
81
70
  const payload = (format.payload ?? {});
82
71
  const missing = [];
@@ -84,39 +73,114 @@ export class AnthropicSemanticMapper {
84
73
  missing.push({ path: 'messages', reason: 'absent' });
85
74
  if (typeof payload.model !== 'string')
86
75
  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
76
  const passthrough = extractMetadataPassthrough(payload.metadata, {
95
77
  prefix: PASSTHROUGH_METADATA_PREFIX,
96
78
  keys: PASSTHROUGH_PARAMETERS
97
79
  });
98
- if (passthrough.passthrough) {
99
- parameters = { ...(parameters || {}), ...passthrough.passthrough };
80
+ const openaiPayload = buildOpenAIChatFromAnthropic(payload);
81
+ const canonicalContext = {
82
+ ...ctx,
83
+ providerProtocol: 'openai-chat',
84
+ entryEndpoint: ctx.entryEndpoint || '/v1/chat/completions'
85
+ };
86
+ const chatEnvelope = await this.chatMapper.toChat({
87
+ protocol: 'openai-chat',
88
+ direction: 'request',
89
+ payload: openaiPayload
90
+ }, canonicalContext);
91
+ const metadata = chatEnvelope.metadata ?? { context: canonicalContext };
92
+ chatEnvelope.metadata = metadata;
93
+ metadata.context = canonicalContext;
94
+ const resolveExtraFields = () => {
95
+ if (!isJsonObject(metadata.extraFields)) {
96
+ metadata.extraFields = {};
97
+ }
98
+ return metadata.extraFields;
99
+ };
100
+ const protocolState = ensureProtocolState(metadata, 'anthropic');
101
+ const systemBlocks = cloneAnthropicSystemBlocks(payload.system);
102
+ if (systemBlocks) {
103
+ protocolState.systemBlocks = systemBlocks;
100
104
  }
101
- const metadata = { context: ctx };
102
105
  if (payload.tools && Array.isArray(payload.tools) && payload.tools.length === 0) {
103
106
  metadata.toolsFieldPresent = true;
107
+ resolveExtraFields().toolsFieldPresent = true;
108
+ }
109
+ const aliasMap = buildAnthropicToolAliasMap(payload.tools);
110
+ if (aliasMap) {
111
+ const extraFields = resolveExtraFields();
112
+ ctx.anthropicToolNameMap = aliasMap;
113
+ canonicalContext.anthropicToolNameMap = aliasMap;
114
+ metadata.anthropicToolNameMap = aliasMap;
115
+ extraFields.anthropicToolNameMap = aliasMap;
116
+ }
117
+ if (Array.isArray(payload.messages) && payload.messages.length) {
118
+ const shapes = payload.messages.map((entry) => {
119
+ if (!entry || typeof entry !== 'object') {
120
+ return 'unknown';
121
+ }
122
+ const rawContent = entry.content;
123
+ if (typeof rawContent === 'string') {
124
+ return 'string';
125
+ }
126
+ if (Array.isArray(rawContent)) {
127
+ return 'array';
128
+ }
129
+ if (rawContent === null || rawContent === undefined) {
130
+ return 'null';
131
+ }
132
+ return typeof rawContent;
133
+ });
134
+ const extraFields = resolveExtraFields();
135
+ const mirrorNode = extraFields.anthropicMirror && typeof extraFields.anthropicMirror === 'object'
136
+ ? extraFields.anthropicMirror
137
+ : {};
138
+ mirrorNode.messageContentShape = shapes;
139
+ extraFields.anthropicMirror = mirrorNode;
104
140
  }
105
141
  if (missing.length) {
106
- metadata.missingFields = missing;
142
+ metadata.missingFields = Array.isArray(metadata.missingFields)
143
+ ? [...metadata.missingFields, ...missing]
144
+ : missing;
107
145
  }
108
- if (passthrough.metadata) {
109
- metadata.providerMetadata = passthrough.metadata;
146
+ const providerMetadata = passthrough.metadata ??
147
+ (payload.metadata && isJsonObject(payload.metadata) ? jsonClone(payload.metadata) : undefined);
148
+ if (providerMetadata) {
149
+ metadata.providerMetadata = providerMetadata;
110
150
  }
111
- else if (payload.metadata && isJsonObject(payload.metadata)) {
112
- metadata.providerMetadata = jsonClone(payload.metadata);
151
+ const mergedParameters = { ...(chatEnvelope.parameters ?? {}) };
152
+ const mergeParameters = (source) => {
153
+ if (!source) {
154
+ return;
155
+ }
156
+ for (const [key, value] of Object.entries(source)) {
157
+ if (mergedParameters[key] !== undefined) {
158
+ continue;
159
+ }
160
+ mergedParameters[key] = jsonClone(value);
161
+ }
162
+ };
163
+ mergeParameters(collectParameters(payload));
164
+ if (providerMetadata) {
165
+ mergedParameters.metadata = jsonClone(providerMetadata);
166
+ }
167
+ if (passthrough.passthrough) {
168
+ for (const [key, value] of Object.entries(passthrough.passthrough)) {
169
+ mergedParameters[key] = jsonClone(value);
170
+ }
171
+ }
172
+ if (Object.keys(mergedParameters).length) {
173
+ chatEnvelope.parameters = mergedParameters;
174
+ }
175
+ else {
176
+ delete chatEnvelope.parameters;
113
177
  }
114
178
  try {
115
179
  const bridgePolicy = resolveBridgePolicy({ protocol: 'anthropic-messages' });
116
180
  const actions = resolvePolicyActions(bridgePolicy, 'request_inbound');
117
181
  if (actions?.length) {
118
182
  const actionState = createBridgeActionState({
119
- messages: messages,
183
+ messages: chatEnvelope.messages,
120
184
  rawRequest: payload,
121
185
  metadata: metadata
122
186
  });
@@ -133,13 +197,7 @@ export class AnthropicSemanticMapper {
133
197
  catch {
134
198
  // best-effort metadata extraction
135
199
  }
136
- return {
137
- messages,
138
- tools,
139
- toolOutputs,
140
- parameters,
141
- metadata
142
- };
200
+ return chatEnvelope;
143
201
  }
144
202
  async fromChat(chat, ctx) {
145
203
  const model = chat.parameters?.model;
@@ -147,11 +205,21 @@ export class AnthropicSemanticMapper {
147
205
  throw new Error('ChatEnvelope.parameters.model is required for anthropic-messages outbound conversion');
148
206
  }
149
207
  const baseRequest = {
150
- ...(chat.parameters || {}),
151
208
  model,
152
209
  messages: chat.messages,
153
210
  tools: chat.tools
154
211
  };
212
+ const trimmedParameters = chat.parameters && typeof chat.parameters === 'object' ? chat.parameters : undefined;
213
+ if (trimmedParameters) {
214
+ for (const [key, value] of Object.entries(trimmedParameters)) {
215
+ if (ANTHROPIC_TOP_LEVEL_FIELDS.has(key) || key === 'stop') {
216
+ if (key === 'messages' || key === 'tools') {
217
+ continue;
218
+ }
219
+ baseRequest[key] = value;
220
+ }
221
+ }
222
+ }
155
223
  const passthroughMetadata = encodeMetadataPassthrough(chat.parameters, {
156
224
  prefix: PASSTHROUGH_METADATA_PREFIX,
157
225
  keys: PASSTHROUGH_PARAMETERS
@@ -175,8 +243,19 @@ export class AnthropicSemanticMapper {
175
243
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
176
244
  baseRequest.tools = [];
177
245
  }
246
+ const protocolState = getProtocolState(chat.metadata, 'anthropic');
247
+ if (protocolState?.systemBlocks !== undefined) {
248
+ baseRequest.system = jsonClone(protocolState.systemBlocks);
249
+ }
250
+ if (chat.metadata &&
251
+ typeof chat.metadata === 'object' &&
252
+ chat.metadata.extraFields &&
253
+ typeof chat.metadata.extraFields === 'object' &&
254
+ chat.metadata.extraFields.anthropicMirror) {
255
+ baseRequest.__anthropicMirror = jsonClone(chat.metadata.extraFields.anthropicMirror ?? {});
256
+ }
178
257
  const payloadSource = buildAnthropicRequestFromOpenAIChat(baseRequest);
179
- const payload = JSON.parse(JSON.stringify(payloadSource));
258
+ const payload = sanitizeAnthropicPayload(JSON.parse(JSON.stringify(payloadSource)));
180
259
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
181
260
  payload.tools = [];
182
261
  }
@@ -210,6 +289,7 @@ export class AnthropicSemanticMapper {
210
289
  catch {
211
290
  // ignore metadata propagation failures
212
291
  }
292
+ sanitizeAnthropicPayload(payload);
213
293
  return {
214
294
  protocol: 'anthropic-messages',
215
295
  direction: 'response',