@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
@@ -2,6 +2,7 @@ import { isJsonObject, jsonClone } from '../types/json.js';
2
2
  import { createBridgeActionState, runBridgeActionPipeline } from '../../shared/bridge-actions.js';
3
3
  import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-policies.js';
4
4
  import { normalizeChatMessageContent } from '../../shared/chat-output-normalizer.js';
5
+ import { ensureProtocolState } from '../../shared/protocol-state.js';
5
6
  const CHAT_PARAMETER_KEYS = [
6
7
  'model',
7
8
  'temperature',
@@ -25,7 +26,9 @@ const KNOWN_TOP_LEVEL_FIELDS = new Set([
25
26
  'messages',
26
27
  'tools',
27
28
  'tool_outputs',
28
- ...CHAT_PARAMETER_KEYS
29
+ ...CHAT_PARAMETER_KEYS,
30
+ 'stageExpectations',
31
+ 'stages'
29
32
  ]);
30
33
  function flattenSystemContent(content) {
31
34
  if (typeof content === 'string')
@@ -56,39 +59,47 @@ function normalizeToolContent(content) {
56
59
  return String(content ?? '');
57
60
  }
58
61
  }
62
+ function recordToolCallIssues(message, messageIndex, missing) {
63
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : undefined;
64
+ if (!toolCalls?.length)
65
+ return;
66
+ toolCalls.forEach((entry, callIndex) => {
67
+ if (!isJsonObject(entry)) {
68
+ missing.push({
69
+ path: `messages[${messageIndex}].tool_calls[${callIndex}]`,
70
+ reason: 'invalid_tool_call_entry',
71
+ originalValue: jsonClone(entry)
72
+ });
73
+ return;
74
+ }
75
+ const fnBlock = entry.function;
76
+ if (!isJsonObject(fnBlock)) {
77
+ missing.push({
78
+ path: `messages[${messageIndex}].tool_calls[${callIndex}].function`,
79
+ reason: 'missing_tool_function',
80
+ originalValue: jsonClone(fnBlock)
81
+ });
82
+ return;
83
+ }
84
+ const fnName = fnBlock.name;
85
+ if (typeof fnName !== 'string' || !fnName.trim().length) {
86
+ missing.push({
87
+ path: `messages[${messageIndex}].tool_calls[${callIndex}].function.name`,
88
+ reason: 'missing_tool_name'
89
+ });
90
+ }
91
+ });
92
+ }
59
93
  function collectSystemRawBlocks(raw) {
60
94
  if (!Array.isArray(raw))
61
95
  return undefined;
62
96
  const blocks = [];
63
- const pushText = (text) => {
64
- if (typeof text === 'string' && text.trim().length) {
65
- blocks.push({ type: 'text', text });
66
- }
67
- };
68
97
  raw.forEach((entry) => {
69
98
  if (!isJsonObject(entry))
70
99
  return;
71
- if (entry.role !== 'system')
72
- return;
73
- const content = entry.content;
74
- if (typeof content === 'string') {
75
- pushText(content);
76
- return;
77
- }
78
- if (Array.isArray(content)) {
79
- content.forEach((part) => {
80
- if (typeof part === 'string') {
81
- pushText(part);
82
- }
83
- else if (isJsonObject(part) && typeof part.text === 'string') {
84
- pushText(part.text);
85
- }
86
- });
100
+ if (String(entry.role ?? '').toLowerCase() !== 'system')
87
101
  return;
88
- }
89
- if (isJsonObject(content) && typeof content.text === 'string') {
90
- pushText(content.text);
91
- }
102
+ blocks.push(jsonClone(entry));
92
103
  });
93
104
  return blocks.length ? blocks : undefined;
94
105
  }
@@ -120,7 +131,8 @@ function normalizeChatMessages(raw) {
120
131
  const chatMessage = value;
121
132
  if (roleValue !== 'system' && roleValue !== 'tool') {
122
133
  const normalizedContent = normalizeChatMessageContent(chatMessage.content);
123
- if (normalizedContent.contentText !== undefined) {
134
+ const shouldOverwriteContent = !Array.isArray(chatMessage.content);
135
+ if (shouldOverwriteContent && normalizedContent.contentText !== undefined) {
124
136
  chatMessage.content = normalizedContent.contentText;
125
137
  }
126
138
  if (typeof normalizedContent.reasoningText === 'string' && normalizedContent.reasoningText.trim().length) {
@@ -128,6 +140,10 @@ function normalizeChatMessages(raw) {
128
140
  }
129
141
  }
130
142
  norm.messages.push(chatMessage);
143
+ const toolCallCandidate = value.tool_calls;
144
+ if (Array.isArray(toolCallCandidate) && toolCallCandidate.length) {
145
+ recordToolCallIssues(value, index, norm.missingFields);
146
+ }
131
147
  if (roleValue === 'system') {
132
148
  const segment = flattenSystemContent(chatMessage.content);
133
149
  if (segment.trim().length) {
@@ -197,6 +213,29 @@ function extractParameters(body) {
197
213
  }
198
214
  return Object.keys(params).length ? params : undefined;
199
215
  }
216
+ function collectExtraFields(body) {
217
+ const extras = {};
218
+ for (const [key, value] of Object.entries(body)) {
219
+ if (KNOWN_TOP_LEVEL_FIELDS.has(key)) {
220
+ continue;
221
+ }
222
+ if (value !== undefined) {
223
+ extras[key] = jsonClone(value);
224
+ }
225
+ }
226
+ return Object.keys(extras).length ? extras : undefined;
227
+ }
228
+ function applyExtraFields(body, metadata) {
229
+ if (!metadata || !metadata.extraFields || !isJsonObject(metadata.extraFields)) {
230
+ return;
231
+ }
232
+ for (const [key, value] of Object.entries(metadata.extraFields)) {
233
+ if (body[key] !== undefined) {
234
+ continue;
235
+ }
236
+ body[key] = jsonClone(value);
237
+ }
238
+ }
200
239
  export class ChatSemanticMapper {
201
240
  async toChat(format, ctx) {
202
241
  const payload = (format.payload ?? {});
@@ -213,12 +252,17 @@ export class ChatSemanticMapper {
213
252
  metadata.systemInstructions = normalized.systemSegments;
214
253
  }
215
254
  const rawSystemBlocks = collectSystemRawBlocks(payload.messages);
216
- if (!metadata.rawSystem && rawSystemBlocks) {
217
- metadata.rawSystem = rawSystemBlocks;
255
+ if (rawSystemBlocks) {
256
+ const protocolState = ensureProtocolState(metadata, 'openai');
257
+ protocolState.systemMessages = jsonClone(rawSystemBlocks);
218
258
  }
219
259
  if (normalized.missingFields.length) {
220
260
  metadata.missingFields = normalized.missingFields;
221
261
  }
262
+ const extraFields = collectExtraFields(payload);
263
+ if (extraFields) {
264
+ metadata.extraFields = extraFields;
265
+ }
222
266
  try {
223
267
  const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-chat' });
224
268
  const actions = resolvePolicyActions(bridgePolicy, 'request_inbound');
@@ -258,6 +302,7 @@ export class ChatSemanticMapper {
258
302
  tools: chat.tools ?? (chat.metadata?.toolsFieldPresent ? [] : undefined),
259
303
  ...(chat.parameters || {})
260
304
  };
305
+ applyExtraFields(payload, chat.metadata);
261
306
  try {
262
307
  const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-chat' });
263
308
  const actions = resolvePolicyActions(bridgePolicy, 'request_outbound');
@@ -288,17 +333,12 @@ export class ChatSemanticMapper {
288
333
  catch {
289
334
  // ignore policy failures
290
335
  }
291
- if (chat.metadata?.rawSystem !== undefined) {
292
- try {
293
- payload.__rcc_raw_system = JSON.stringify(chat.metadata.rawSystem);
294
- }
295
- catch {
296
- // ignore serialization errors
297
- }
298
- }
299
- if (chat.toolOutputs?.length) {
300
- payload.tool_outputs = chat.toolOutputs;
301
- }
336
+ // Do not forward tool_outputs to provider wire formats. OpenAI Chat
337
+ // endpoints expect tool results to appear as tool role messages, and
338
+ // sending the legacy top-level field causes upstream HTTP 400 responses.
339
+ // Concrete translation happens earlier when responses input is unfolded
340
+ // into ChatEnvelope.messages, so the provider request only needs the
341
+ // canonical message list.
302
342
  if (payload.max_tokens === undefined && typeof payload.max_output_tokens === 'number') {
303
343
  payload.max_tokens = payload.max_output_tokens;
304
344
  delete payload.max_output_tokens;
@@ -5,6 +5,7 @@ import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-p
5
5
  import { encodeMetadataPassthrough, extractMetadataPassthrough } from '../../shared/metadata-passthrough.js';
6
6
  import { mapBridgeToolsToChat, mapChatToolsToBridge } from '../../shared/tool-mapping.js';
7
7
  import { prepareGeminiToolsForBridge, buildGeminiToolsFromBridge } from '../../shared/gemini-tool-utils.js';
8
+ import { ensureProtocolState, getProtocolState } from '../../shared/protocol-state.js';
8
9
  const GENERATION_CONFIG_KEYS = [
9
10
  { source: 'temperature', target: 'temperature' },
10
11
  { source: 'topP', target: 'top_p' },
@@ -153,11 +154,9 @@ function buildGeminiRequestFromChat(chat, metadata) {
153
154
  model: chat.parameters?.model || 'models/gemini-pro',
154
155
  contents
155
156
  };
156
- if (metadata?.rawSystem && isJsonObject(metadata.rawSystem) && String(metadata.rawSystem.role || '').toLowerCase() === 'system') {
157
- request.systemInstruction = metadata.rawSystem;
158
- }
159
- else if (metadata?.rawSystemInstruction !== undefined) {
160
- request.systemInstruction = metadata.rawSystemInstruction;
157
+ const geminiState = getProtocolState(metadata, 'gemini');
158
+ if (geminiState?.systemInstruction !== undefined) {
159
+ request.systemInstruction = jsonClone(geminiState.systemInstruction);
161
160
  }
162
161
  else if (metadata?.systemInstructions && Array.isArray(metadata.systemInstructions)) {
163
162
  const sysBlocks = metadata.systemInstructions
@@ -192,15 +191,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
192
191
  request.metadata = request.metadata ?? {};
193
192
  request.metadata.__rcc_stream = chat.parameters.stream;
194
193
  }
195
- if (metadata?.rawSystem !== undefined) {
196
- request.metadata = request.metadata ?? {};
197
- try {
198
- request.metadata.__rcc_raw_system = JSON.stringify(metadata.rawSystem);
199
- }
200
- catch {
201
- // ignore serialization errors
202
- }
203
- }
204
194
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
205
195
  request.metadata = request.metadata ?? {};
206
196
  request.metadata.__rcc_tools_field_present = '1';
@@ -285,8 +275,7 @@ export class GeminiSemanticMapper {
285
275
  }
286
276
  if (payload.systemInstruction !== undefined) {
287
277
  const rawSystem = jsonClone(payload.systemInstruction);
288
- metadata.rawSystemInstruction = rawSystem;
289
- metadata.rawSystem = rawSystem;
278
+ ensureProtocolState(metadata, 'gemini').systemInstruction = rawSystem;
290
279
  }
291
280
  if (payload.safetySettings) {
292
281
  metadata.safetySettings = jsonClone(payload.safetySettings);
@@ -329,7 +318,6 @@ export class GeminiSemanticMapper {
329
318
  if (providerMetadataSource) {
330
319
  const providerMetadata = jsonClone(providerMetadataSource);
331
320
  let toolsFieldPresent = false;
332
- let rawSystemValue;
333
321
  if (isJsonObject(providerMetadata)) {
334
322
  delete providerMetadata.__rcc_stream;
335
323
  if (Object.prototype.hasOwnProperty.call(providerMetadata, '__rcc_tools_field_present')) {
@@ -338,24 +326,12 @@ export class GeminiSemanticMapper {
338
326
  delete providerMetadata.__rcc_tools_field_present;
339
327
  }
340
328
  if (Object.prototype.hasOwnProperty.call(providerMetadata, '__rcc_raw_system')) {
341
- const rawSystemSentinel = providerMetadata.__rcc_raw_system;
342
329
  delete providerMetadata.__rcc_raw_system;
343
- if (typeof rawSystemSentinel === 'string') {
344
- try {
345
- rawSystemValue = JSON.parse(rawSystemSentinel);
346
- }
347
- catch {
348
- rawSystemValue = undefined;
349
- }
350
- }
351
330
  }
352
331
  }
353
332
  if (toolsFieldPresent) {
354
333
  metadata.toolsFieldPresent = true;
355
334
  }
356
- if (rawSystemValue !== undefined) {
357
- metadata.rawSystem = rawSystemValue;
358
- }
359
335
  metadata.providerMetadata = providerMetadata;
360
336
  }
361
337
  return {
@@ -12,10 +12,7 @@ const RESPONSES_PARAMETER_KEYS = [
12
12
  'response_format',
13
13
  'tool_choice',
14
14
  'parallel_tool_calls',
15
- 'metadata',
16
- 'store',
17
15
  'user',
18
- 'include',
19
16
  'logit_bias',
20
17
  'seed',
21
18
  'stop',
@@ -211,21 +208,27 @@ export class ResponsesSemanticMapper {
211
208
  .filter((message) => Boolean(message && typeof message === 'object' && message.role === 'system'))
212
209
  .map(message => serializeSystemContent(message))
213
210
  .filter((content) => typeof content === 'string' && content.length > 0);
214
- const responsesContext = chat.metadata
215
- ? { metadata: chat.metadata, originalSystemMessages }
216
- : { originalSystemMessages };
211
+ const capturedContext = chat.metadata?.responsesContext;
212
+ const responsesContext = isJsonObject(capturedContext)
213
+ ? {
214
+ ...capturedContext,
215
+ originalSystemMessages
216
+ }
217
+ : {
218
+ metadata: (chat.metadata && isJsonObject(chat.metadata) ? chat.metadata : undefined),
219
+ originalSystemMessages
220
+ };
217
221
  const responsesResult = buildResponsesRequestFromChat(requestShape, responsesContext);
218
222
  const responses = responsesResult.request;
219
223
  if (chat.parameters && chat.parameters.stream !== undefined) {
220
224
  responses.stream = chat.parameters.stream;
221
225
  }
222
- if (chat.toolOutputs && chat.toolOutputs.length) {
223
- responses.tool_outputs = chat.toolOutputs.map(entry => ({
224
- tool_call_id: entry.tool_call_id,
225
- output: entry.content,
226
- name: entry.name
227
- }));
228
- }
226
+ // Do not forward ChatEnvelope.toolOutputs to OpenAI Responses create requests.
227
+ // Upstream expects historical tool results to remain inside input[] as
228
+ // tool role messages; sending the legacy top-level `tool_outputs` field
229
+ // causes providers like FAI to reject the request (HTTP 400). Any actual
230
+ // submit_tool_outputs call should be issued via the dedicated endpoint
231
+ // upstream, not through this mapper.
229
232
  try {
230
233
  const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-responses', moduleType: 'openai-responses' });
231
234
  const actions = resolvePolicyActions(bridgePolicy, 'request_outbound');
@@ -0,0 +1,13 @@
1
+ import type { StageRecorder } from './format-adapters/index.js';
2
+ import type { AdapterContext } from './types/chat-envelope.js';
3
+ export interface SnapshotStageRecorderOptions {
4
+ context: AdapterContext;
5
+ endpoint: string;
6
+ }
7
+ export declare class SnapshotStageRecorder implements StageRecorder {
8
+ private readonly options;
9
+ private readonly writer?;
10
+ constructor(options: SnapshotStageRecorderOptions);
11
+ record(stage: string, payload: object): void;
12
+ }
13
+ export declare function createSnapshotRecorder(context: AdapterContext, endpoint: string): StageRecorder;
@@ -1,69 +1,109 @@
1
- import { writeSnapshotViaHooks } from '../shared/snapshot-hooks.js';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import fs from 'node:fs/promises';
1
+ import { createSnapshotWriter } from '../shared/snapshot-utils.js';
2
+ import { jsonClone } from './types/json.js';
5
3
  export class SnapshotStageRecorder {
6
4
  options;
5
+ writer;
7
6
  constructor(options) {
8
7
  this.options = options;
8
+ this.writer = createSnapshotWriter({
9
+ requestId: options.context.requestId,
10
+ endpoint: options.endpoint
11
+ });
9
12
  }
10
13
  record(stage, payload) {
11
- void writeSnapshotFile({
12
- endpoint: this.options.endpoint,
13
- stage,
14
- requestId: this.options.context.requestId,
15
- data: payload
16
- });
17
- void writeSnapshotViaHooks({
18
- endpoint: this.options.endpoint,
19
- stage,
20
- requestId: this.options.context.requestId,
21
- data: payload,
22
- verbosity: 'verbose'
23
- }).catch(() => {
24
- /* ignore hook errors */
25
- });
14
+ if (!this.writer) {
15
+ return;
16
+ }
17
+ const normalized = normalizeStagePayload(stage, payload);
18
+ if (!normalized) {
19
+ return;
20
+ }
21
+ try {
22
+ this.writer(stage, normalized);
23
+ }
24
+ catch {
25
+ // ignore snapshot write errors
26
+ }
26
27
  }
27
28
  }
28
29
  export function createSnapshotRecorder(context, endpoint) {
29
30
  return new SnapshotStageRecorder({ context, endpoint });
30
31
  }
31
- const SNAPSHOT_BASE = path.join(os.homedir(), '.routecodex', 'codex-samples');
32
- function mapEndpointToFolder(endpoint) {
33
- const ep = String(endpoint || '').toLowerCase();
34
- if (ep.includes('/responses'))
35
- return 'openai-responses';
36
- if (ep.includes('/messages'))
37
- return 'anthropic-messages';
38
- return 'openai-chat';
32
+ const STAGE_KIND_MAP = {
33
+ req_inbound_stage2_semantic_map: 'request_inbound',
34
+ req_outbound_stage1_semantic_map: 'request_outbound',
35
+ resp_inbound_stage3_semantic_map: 'response_inbound',
36
+ resp_outbound_stage1_client_remap: 'response_outbound'
37
+ };
38
+ function normalizeStagePayload(stage, payload) {
39
+ const kind = STAGE_KIND_MAP[stage];
40
+ if (!kind) {
41
+ return payload;
42
+ }
43
+ if ((kind === 'request_inbound' || kind === 'request_outbound') && isChatEnvelope(payload)) {
44
+ return buildOpenAIChatSnapshot(payload);
45
+ }
46
+ if (kind === 'response_inbound' || kind === 'response_outbound') {
47
+ return cloneJson(payload);
48
+ }
49
+ return payload;
39
50
  }
40
- async function ensureDir(dir) {
41
- try {
42
- await fs.mkdir(dir, { recursive: true });
51
+ function isChatEnvelope(value) {
52
+ return Boolean(value &&
53
+ typeof value === 'object' &&
54
+ Array.isArray(value.messages) &&
55
+ value.metadata &&
56
+ typeof value.metadata === 'object');
57
+ }
58
+ function buildOpenAIChatSnapshot(envelope) {
59
+ const snapshot = {};
60
+ if (envelope.parameters && Object.keys(envelope.parameters).length) {
61
+ Object.assign(snapshot, jsonClone(envelope.parameters));
43
62
  }
44
- catch {
45
- // ignore
63
+ snapshot.messages = jsonClone(envelope.messages);
64
+ if (envelope.tools && envelope.tools.length) {
65
+ snapshot.tools = jsonClone(envelope.tools);
46
66
  }
67
+ if (envelope.toolOutputs && envelope.toolOutputs.length) {
68
+ snapshot.tool_outputs = jsonClone(envelope.toolOutputs);
69
+ }
70
+ const meta = buildMetaSnapshot(envelope.metadata);
71
+ if (meta) {
72
+ snapshot.meta = meta;
73
+ }
74
+ return snapshot;
75
+ }
76
+ function buildMetaSnapshot(metadata) {
77
+ if (!metadata || typeof metadata !== 'object') {
78
+ return undefined;
79
+ }
80
+ const meta = {};
81
+ if (metadata.context) {
82
+ meta.context = jsonClone(metadata.context);
83
+ }
84
+ if (Array.isArray(metadata.missingFields) && metadata.missingFields.length) {
85
+ meta.missing_fields = jsonClone(metadata.missingFields);
86
+ }
87
+ const extraKeys = Object.keys(metadata).filter((key) => key !== 'context' && key !== 'missingFields');
88
+ if (extraKeys.length) {
89
+ const extras = {};
90
+ for (const key of extraKeys) {
91
+ const value = metadata[key];
92
+ if (value !== undefined) {
93
+ extras[key] = jsonClone(value);
94
+ }
95
+ }
96
+ if (Object.keys(extras).length) {
97
+ meta.extra = extras;
98
+ }
99
+ }
100
+ return Object.keys(meta).length ? meta : undefined;
47
101
  }
48
- async function writeSnapshotFile(options) {
102
+ function cloneJson(payload) {
49
103
  try {
50
- const folder = mapEndpointToFolder(options.endpoint);
51
- const dir = path.join(SNAPSHOT_BASE, folder);
52
- await ensureDir(dir);
53
- const safeStage = options.stage.replace(/[^\w.-]/g, '_');
54
- const safeRequestId = options.requestId.replace(/[^\w.-]/g, '_');
55
- const file = path.join(dir, `${safeRequestId}_${safeStage}.json`);
56
- const payload = {
57
- meta: {
58
- stage: options.stage,
59
- timestamp: Date.now(),
60
- endpoint: options.endpoint
61
- },
62
- body: options.data
63
- };
64
- await fs.writeFile(file, JSON.stringify(payload, null, 2), 'utf-8');
104
+ return JSON.parse(JSON.stringify(payload));
65
105
  }
66
- catch (error) {
67
- console.warn('[hub-snapshot] failed to write local snapshot', error);
106
+ catch {
107
+ return payload;
68
108
  }
69
109
  }
@@ -1,3 +1,4 @@
1
+ import { isJsonObject, jsonClone } from './types/json.js';
1
2
  const HUB_CAPTURE_KEY = '__hub_capture';
2
3
  export function chatEnvelopeToStandardized(chat, options) {
3
4
  const parameters = { ...(chat.parameters ?? {}) };
@@ -15,8 +16,8 @@ export function chatEnvelopeToStandardized(chat, options) {
15
16
  if (isJsonObject(chat.metadata?.extraFields)) {
16
17
  hubState.extraFields = chat.metadata?.extraFields;
17
18
  }
18
- if (chat.metadata?.rawSystem !== undefined) {
19
- hubState.rawSystem = chat.metadata.rawSystem;
19
+ if (isJsonObject(chat.metadata?.protocolState)) {
20
+ hubState.protocolState = chat.metadata?.protocolState;
20
21
  }
21
22
  hubState.context = options.adapterContext;
22
23
  if (hubStateContextPopulated(hubState)) {
@@ -41,13 +42,16 @@ export function chatEnvelopeToStandardized(chat, options) {
41
42
  export function standardizedToChatEnvelope(request, options) {
42
43
  const adapterContext = options.adapterContext;
43
44
  const hubState = extractHubCapture(request);
44
- const messages = request.messages.map((message) => ({
45
- role: message.role,
46
- content: message.content ?? '',
47
- tool_calls: mapToolCalls(message.tool_calls),
48
- tool_call_id: message.tool_call_id,
49
- name: message.name
50
- }));
45
+ const messages = request.messages.map((message) => {
46
+ const restoredContent = restoreMessageContent(message.content);
47
+ return {
48
+ role: message.role,
49
+ content: restoredContent,
50
+ tool_calls: mapToolCalls(message.tool_calls),
51
+ tool_call_id: message.tool_call_id,
52
+ name: message.name
53
+ };
54
+ });
51
55
  const tools = mapStandardizedTools(request.tools);
52
56
  const parameters = {
53
57
  ...(request.parameters ?? {}),
@@ -68,8 +72,8 @@ export function standardizedToChatEnvelope(request, options) {
68
72
  if (hubState?.extraFields) {
69
73
  metadata.extraFields = hubState.extraFields;
70
74
  }
71
- if (hubState?.rawSystem !== undefined) {
72
- metadata.rawSystem = hubState.rawSystem;
75
+ if (hubState?.protocolState) {
76
+ metadata.protocolState = hubState.protocolState;
73
77
  }
74
78
  return {
75
79
  messages,
@@ -88,7 +92,7 @@ function extractModel(parameters) {
88
92
  function normalizeChatMessage(message) {
89
93
  const normalized = {
90
94
  role: message.role,
91
- content: normalizeContent(message.content)
95
+ content: cloneMessageContent(message.content)
92
96
  };
93
97
  if (Array.isArray(message.tool_calls) && message.tool_calls.length) {
94
98
  normalized.tool_calls = message.tool_calls
@@ -103,7 +107,7 @@ function normalizeChatMessage(message) {
103
107
  }
104
108
  return normalized;
105
109
  }
106
- function normalizeContent(content) {
110
+ function cloneMessageContent(content) {
107
111
  if (content === undefined || content === null) {
108
112
  return null;
109
113
  }
@@ -111,21 +115,35 @@ function normalizeContent(content) {
111
115
  return content;
112
116
  }
113
117
  if (Array.isArray(content)) {
114
- const parts = content
115
- .map((part) => {
116
- if (part && typeof part === 'object') {
117
- if (typeof part.text === 'string') {
118
- return String(part.text);
119
- }
120
- return JSON.stringify(part);
121
- }
122
- return '';
123
- })
124
- .filter(Boolean);
125
- return parts.join('\n');
118
+ return cloneContentArray(content);
126
119
  }
127
120
  return String(content);
128
121
  }
122
+ function restoreMessageContent(content) {
123
+ if (content === undefined) {
124
+ return undefined;
125
+ }
126
+ if (content === null) {
127
+ return null;
128
+ }
129
+ if (typeof content === 'string') {
130
+ return content;
131
+ }
132
+ return cloneContentArray(content);
133
+ }
134
+ function cloneContentArray(parts) {
135
+ return parts.map((part) => {
136
+ if (part && typeof part === 'object') {
137
+ return jsonClone(part);
138
+ }
139
+ const text = typeof part === 'string' ? part : String(part ?? '');
140
+ const fallback = {
141
+ type: 'text',
142
+ text
143
+ };
144
+ return fallback;
145
+ });
146
+ }
129
147
  function normalizeToolCall(toolCall) {
130
148
  if (!toolCall || typeof toolCall !== 'object') {
131
149
  return null;
@@ -137,20 +155,16 @@ function normalizeToolCall(toolCall) {
137
155
  if (type !== 'function') {
138
156
  return null;
139
157
  }
140
- if (!fn || typeof fn !== 'object') {
141
- return null;
142
- }
143
- const fnName = fn.name;
144
- if (typeof fnName !== 'string' || !fnName.trim()) {
145
- return null;
146
- }
147
- const args = fn.arguments;
158
+ const fnRecord = fn && typeof fn === 'object' ? fn : undefined;
159
+ const rawName = typeof fnRecord?.name === 'string' ? fnRecord.name : '';
160
+ const normalizedName = rawName.trim();
161
+ const args = fnRecord?.arguments;
148
162
  const serializedArgs = typeof args === 'string' ? args : safeStringify(args ?? {});
149
163
  return {
150
164
  id: id.trim(),
151
165
  type: 'function',
152
166
  function: {
153
- name: fnName.trim(),
167
+ name: normalizedName.length ? normalizedName : rawName,
154
168
  arguments: serializedArgs
155
169
  }
156
170
  };
@@ -218,9 +232,6 @@ function normalizeTools(tools) {
218
232
  }
219
233
  return normalized;
220
234
  }
221
- function isJsonObject(value) {
222
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
223
- }
224
235
  function extractHubCapture(request) {
225
236
  const captured = request.metadata?.capturedContext;
226
237
  if (!captured || typeof captured !== 'object') {
@@ -247,5 +258,5 @@ function hubStateContextPopulated(state) {
247
258
  (state.missingFields && state.missingFields.length) ||
248
259
  state.providerMetadata ||
249
260
  state.extraFields ||
250
- state.rawSystem !== undefined);
261
+ (state.protocolState && Object.keys(state.protocolState).length));
251
262
  }