@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
@@ -6,26 +6,58 @@ function isRecord(value) {
6
6
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
7
7
  }
8
8
  export function normalizeReasoningInChatPayload(payload) {
9
- if (!payload || !Array.isArray(payload.messages)) {
9
+ if (!payload) {
10
10
  return;
11
11
  }
12
- payload.messages = payload.messages.map((entry) => {
13
- if (!entry || typeof entry !== 'object') {
14
- return entry;
15
- }
16
- const msg = entry;
17
- const normalized = normalizeChatMessageContent(msg.content);
18
- if (normalized.contentText !== undefined) {
19
- msg.content = normalized.contentText;
20
- }
21
- if (normalized.reasoningText && normalized.reasoningText.trim().length) {
22
- msg.reasoning_content = normalized.reasoningText.trim();
23
- }
24
- else {
25
- delete msg.reasoning_content;
26
- }
27
- return msg;
28
- });
12
+ if (Array.isArray(payload.messages)) {
13
+ payload.messages = payload.messages.map(entry => normalizeChatMessageEntry(entry));
14
+ }
15
+ if (Array.isArray(payload.choices)) {
16
+ payload.choices.forEach(choice => {
17
+ normalizeChatChoice(choice);
18
+ });
19
+ }
20
+ }
21
+ function normalizeChatMessageEntry(entry) {
22
+ if (!entry || typeof entry !== 'object') {
23
+ return entry;
24
+ }
25
+ const msg = entry;
26
+ applyNormalizedChatContent(msg);
27
+ return msg;
28
+ }
29
+ function normalizeChatChoice(choice) {
30
+ if (!choice || typeof choice !== 'object') {
31
+ return;
32
+ }
33
+ const record = choice;
34
+ const containers = [];
35
+ if (record.message && typeof record.message === 'object') {
36
+ containers.push(record.message);
37
+ }
38
+ if (record.delta && typeof record.delta === 'object') {
39
+ containers.push(record.delta);
40
+ }
41
+ containers.forEach(container => applyNormalizedChatContent(container));
42
+ }
43
+ function applyNormalizedChatContent(container) {
44
+ const normalized = normalizeChatMessageContent(container.content);
45
+ const role = typeof container.role === 'string' ? container.role : undefined;
46
+ if (normalized.contentText !== undefined && normalized.contentText.trim().length) {
47
+ container.content = normalized.contentText;
48
+ }
49
+ else if (typeof container.reasoning_content === 'string' && container.reasoning_content.trim().length) {
50
+ container.content = container.reasoning_content.trim();
51
+ }
52
+ else if (role && role !== 'system' && role !== 'tool') {
53
+ container.content = '';
54
+ }
55
+ if (normalized.reasoningText && normalized.reasoningText.trim().length) {
56
+ container.reasoning_content = normalized.reasoningText.trim();
57
+ }
58
+ else if ('reasoning_content' in container) {
59
+ delete container.reasoning_content;
60
+ }
29
61
  }
30
62
  export function normalizeReasoningInResponsesPayload(payload, options = { includeOutput: true }) {
31
63
  if (!payload) {
@@ -7,7 +7,7 @@ export interface BuildResponsesOutputOptions {
7
7
  }
8
8
  export interface BuildResponsesOutputResult {
9
9
  outputItems: ResponsesOutputItem[];
10
- outputText: string;
10
+ outputText?: string;
11
11
  status: string;
12
12
  requiredAction?: Record<string, unknown>;
13
13
  usage?: unknown;
@@ -1,17 +1,64 @@
1
1
  import { normalizeFunctionCallId } from './bridge-id-utils.js';
2
2
  import { normalizeContentPart } from './output-content-normalizer.js';
3
3
  import { expandResponsesMessageItem } from '../../sse/shared/responses-output-normalizer.js';
4
+ function appendReasoningSegments(target, raw) {
5
+ if (typeof raw !== 'string' || !raw.length) {
6
+ return;
7
+ }
8
+ const segments = raw.split('\n');
9
+ let previousValue;
10
+ for (const segment of segments) {
11
+ if (!segment.length)
12
+ continue;
13
+ let value = segment;
14
+ if (previousValue) {
15
+ const prevLast = previousValue.charAt(previousValue.length - 1) || '';
16
+ const currFirst = segment.charAt(0) || '';
17
+ const prevWord = /[A-Za-z0-9_]$/.test(prevLast);
18
+ const currWord = /^[A-Za-z0-9]/.test(currFirst);
19
+ const currQuote = currFirst === '"' || currFirst === "'";
20
+ const prevPunct = /[:;]/.test(prevLast);
21
+ const prevEndsWhitespace = /\s$/.test(previousValue);
22
+ const prevSentenceEnd = /[.?!"]$/.test(prevLast);
23
+ if (((prevWord || prevSentenceEnd) && currWord && !prevEndsWhitespace) ||
24
+ (prevPunct && currQuote)) {
25
+ value = ` ${segment}`;
26
+ }
27
+ }
28
+ target.push(value);
29
+ previousValue = value;
30
+ }
31
+ }
4
32
  export function buildResponsesOutputFromChat(options) {
5
33
  const { response, message, requestId, sanitizeFunctionName } = options;
6
34
  const outputItems = [];
7
35
  const allocateOutputId = (prefix) => `${prefix}_${requestId ?? Date.now()}_${outputItems.length + 1}`;
8
36
  const role = message?.role || 'assistant';
9
37
  const content = message?.content;
10
- const reasoningText = typeof message?.reasoning_content === 'string' && message.reasoning_content.trim().length
11
- ? String(message.reasoning_content).trim()
38
+ let toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
39
+ try {
40
+ toolCalls = toolCalls.filter((it) => {
41
+ const nm = (it && typeof it === 'object') ? (it?.function?.name || it.name) : undefined;
42
+ return typeof nm === 'string' && nm.trim().length > 0 && nm.toLowerCase() !== 'tool';
43
+ });
44
+ }
45
+ catch {
46
+ /* ignore */
47
+ }
48
+ const hasToolCalls = toolCalls.length > 0;
49
+ const reasoningChunks = [];
50
+ const preservedReasoning = Array.isArray(response?.__responses_reasoning)
51
+ ? response.__responses_reasoning
12
52
  : undefined;
53
+ if (preservedReasoning && preservedReasoning.length) {
54
+ reasoningChunks.push(...preservedReasoning);
55
+ }
56
+ else {
57
+ appendReasoningSegments(reasoningChunks, message?.reasoning_content);
58
+ }
13
59
  const convertedContent = convertChatContentToResponses(content);
14
- if (message || convertedContent.length > 0 || reasoningText) {
60
+ const shouldEmitMessage = Boolean(message) && (convertedContent.length > 0 || reasoningChunks.length > 0 || !hasToolCalls);
61
+ if (shouldEmitMessage) {
15
62
  const responsesMessage = {
16
63
  id: allocateOutputId('message'),
17
64
  type: 'message',
@@ -22,23 +69,13 @@ export function buildResponsesOutputFromChat(options) {
22
69
  const expandedItems = expandResponsesMessageItem(responsesMessage, {
23
70
  requestId: requestId ?? 'responses_outbound',
24
71
  outputIndex: outputItems.length,
25
- extraReasoning: reasoningText
72
+ extraReasoning: reasoningChunks
26
73
  });
27
74
  for (const expanded of expandedItems) {
28
75
  outputItems.push(expanded);
29
76
  }
30
77
  }
31
- let toolCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
32
78
  const normalizedToolCalls = [];
33
- try {
34
- toolCalls = toolCalls.filter((it) => {
35
- const nm = (it && typeof it === 'object') ? (it?.function?.name || it.name) : undefined;
36
- return typeof nm === 'string' && nm.trim().length > 0 && nm.toLowerCase() !== 'tool';
37
- });
38
- }
39
- catch {
40
- /* ignore */
41
- }
42
79
  let toolFallbackCounter = 0;
43
80
  for (const call of toolCalls) {
44
81
  const outputEntry = buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, outputItems.length, ++toolFallbackCounter);
@@ -52,19 +89,20 @@ export function buildResponsesOutputFromChat(options) {
52
89
  }
53
90
  }
54
91
  const usage = normalizeUsage(response.usage);
55
- const outputText = extractOutputText(convertedContent);
56
- const hasToolCalls = toolCalls.length > 0;
57
- if (hasToolCalls) {
92
+ const outputTextMeta = response?.__responses_output_text_meta;
93
+ const outputText = resolveOutputText(convertedContent, outputTextMeta);
94
+ const hasNormalizedToolCalls = normalizedToolCalls.length > 0;
95
+ if (hasNormalizedToolCalls) {
58
96
  for (const item of outputItems) {
59
97
  if (item.type === 'message') {
60
98
  item.status = 'in_progress';
61
99
  }
62
100
  }
63
101
  }
64
- const requiredAction = normalizedToolCalls.length
102
+ const requiredAction = hasNormalizedToolCalls
65
103
  ? buildRequiredActionFromNormalized(normalizedToolCalls)
66
104
  : undefined;
67
- const status = hasToolCalls ? 'requires_action' : 'completed';
105
+ const status = hasNormalizedToolCalls ? 'requires_action' : 'completed';
68
106
  return {
69
107
  outputItems,
70
108
  outputText,
@@ -121,12 +159,10 @@ function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, b
121
159
  const originalCallId = typeof call.id === 'string' && call.id.trim().length
122
160
  ? String(call.id)
123
161
  : (typeof call.call_id === 'string' && call.call_id.trim().length ? String(call.call_id) : undefined);
124
- const callId = originalCallId && originalCallId.trim().length
125
- ? originalCallId.trim()
126
- : normalizeFunctionCallId({
127
- callId: originalCallId,
128
- fallback: `fc_call_${baseCount + offset}`
129
- });
162
+ const callId = normalizeFunctionCallId({
163
+ callId: originalCallId,
164
+ fallback: `fc_call_${baseCount + offset}`
165
+ });
130
166
  const output = {
131
167
  id: allocateOutputId('function_call'),
132
168
  type: 'function_call',
@@ -177,3 +213,18 @@ function extractOutputText(parts) {
177
213
  const text = normalizedTexts.join('\n').trim();
178
214
  return text.length ? text : '';
179
215
  }
216
+ function resolveOutputText(parts, meta) {
217
+ if (meta && typeof meta === 'object') {
218
+ const hasField = Boolean(meta.hasField);
219
+ if (hasField) {
220
+ const raw = meta.value;
221
+ if (typeof raw === 'string') {
222
+ return raw;
223
+ }
224
+ return '';
225
+ }
226
+ return undefined;
227
+ }
228
+ const text = extractOutputText(parts);
229
+ return text.length ? text : undefined;
230
+ }
@@ -0,0 +1,8 @@
1
+ export interface ResponsesOutputTextMeta {
2
+ hasField: boolean;
3
+ value?: string;
4
+ }
5
+ export declare function registerResponsesReasoning(id: unknown, segments: string[] | undefined): void;
6
+ export declare function consumeResponsesReasoning(id: unknown): string[] | undefined;
7
+ export declare function registerResponsesOutputTextMeta(id: unknown, meta: ResponsesOutputTextMeta | undefined): void;
8
+ export declare function consumeResponsesOutputTextMeta(id: unknown): ResponsesOutputTextMeta | undefined;
@@ -0,0 +1,61 @@
1
+ const registry = new Map();
2
+ function ensureEntry(id) {
3
+ let entry = registry.get(id);
4
+ if (!entry) {
5
+ entry = {};
6
+ registry.set(id, entry);
7
+ }
8
+ return entry;
9
+ }
10
+ function pruneEntry(id) {
11
+ const entry = registry.get(id);
12
+ if (!entry)
13
+ return;
14
+ if (!entry.reasoning && !entry.outputText) {
15
+ registry.delete(id);
16
+ }
17
+ }
18
+ export function registerResponsesReasoning(id, segments) {
19
+ if (typeof id !== 'string')
20
+ return;
21
+ if (!Array.isArray(segments) || segments.length === 0)
22
+ return;
23
+ const entry = ensureEntry(id);
24
+ entry.reasoning = [...segments];
25
+ }
26
+ export function consumeResponsesReasoning(id) {
27
+ if (typeof id !== 'string')
28
+ return undefined;
29
+ const entry = registry.get(id);
30
+ if (!entry?.reasoning)
31
+ return undefined;
32
+ const value = [...entry.reasoning];
33
+ entry.reasoning = undefined;
34
+ pruneEntry(id);
35
+ return value;
36
+ }
37
+ export function registerResponsesOutputTextMeta(id, meta) {
38
+ if (typeof id !== 'string')
39
+ return;
40
+ if (!meta)
41
+ return;
42
+ const entry = ensureEntry(id);
43
+ entry.outputText = {
44
+ hasField: Boolean(meta.hasField),
45
+ value: typeof meta.value === 'string' ? meta.value : undefined
46
+ };
47
+ }
48
+ export function consumeResponsesOutputTextMeta(id) {
49
+ if (typeof id !== 'string')
50
+ return undefined;
51
+ const entry = registry.get(id);
52
+ if (!entry?.outputText)
53
+ return undefined;
54
+ const value = {
55
+ hasField: Boolean(entry.outputText.hasField),
56
+ value: entry.outputText.value
57
+ };
58
+ entry.outputText = undefined;
59
+ pruneEntry(id);
60
+ return value;
61
+ }
@@ -135,6 +135,24 @@ function unwrapResponsesResponse(payload) {
135
135
  }
136
136
  return undefined;
137
137
  }
138
+ function collectRawReasoningSegments(response) {
139
+ const segments = [];
140
+ const outputItems = Array.isArray(response.output) ? response.output : [];
141
+ for (const item of outputItems) {
142
+ if (!item || typeof item !== 'object')
143
+ continue;
144
+ const type = typeof item.type === 'string' ? String(item.type).toLowerCase() : '';
145
+ if (type !== 'reasoning')
146
+ continue;
147
+ const content = Array.isArray(item.content) ? item.content : [];
148
+ for (const part of content) {
149
+ if (part && typeof part === 'object' && typeof part.text === 'string') {
150
+ segments.push(part.text);
151
+ }
152
+ }
153
+ }
154
+ return segments;
155
+ }
138
156
  export function buildChatResponseFromResponses(payload) {
139
157
  if (!payload || typeof payload !== 'object')
140
158
  return payload;
@@ -152,6 +170,7 @@ export function buildChatResponseFromResponses(payload) {
152
170
  const usage = response.usage;
153
171
  const toolCalls = collectToolCallsFromResponses(response);
154
172
  const { textParts, reasoningParts } = extractOutputSegments(response);
173
+ const rawReasoningSegments = collectRawReasoningSegments(response);
155
174
  const explicitOutput = typeof response.output_text === 'string' && response.output_text.trim().length
156
175
  ? sanitizeReasoningTaggedText(response.output_text)
157
176
  : undefined;
@@ -185,8 +204,9 @@ export function buildChatResponseFromResponses(payload) {
185
204
  if (toolCalls.length) {
186
205
  message.tool_calls = toolCalls;
187
206
  }
188
- if (reasoningParts.length) {
189
- message.reasoning_content = reasoningParts.join('\n');
207
+ const reasoningSegments = rawReasoningSegments.length ? rawReasoningSegments : reasoningParts;
208
+ if (reasoningSegments.length) {
209
+ message.reasoning_content = reasoningSegments.join('\n');
190
210
  }
191
211
  const finishReason = resolveFinishReason(response, toolCalls);
192
212
  const chat = {
@@ -202,6 +222,16 @@ export function buildChatResponseFromResponses(payload) {
202
222
  }
203
223
  ]
204
224
  };
225
+ const hasOutputTextField = Object.prototype.hasOwnProperty.call(response, 'output_text');
226
+ if (rawReasoningSegments.length) {
227
+ chat.__responses_reasoning = rawReasoningSegments;
228
+ }
229
+ chat.__responses_output_text_meta = {
230
+ hasField: hasOutputTextField,
231
+ value: hasOutputTextField && typeof response.output_text === 'string'
232
+ ? sanitizeReasoningTaggedText(response.output_text)
233
+ : undefined
234
+ };
205
235
  if (usage !== undefined) {
206
236
  chat.usage = usage;
207
237
  }
@@ -68,10 +68,36 @@ export function resolveToolCallIdStyle(metadata) {
68
68
  export function stripInternalToolingMetadata(metadata) {
69
69
  if (!metadata || typeof metadata !== 'object')
70
70
  return;
71
- if ('toolCallIdStyle' in metadata) {
72
- delete metadata.toolCallIdStyle;
71
+ const record = metadata;
72
+ if ('toolCallIdStyle' in record) {
73
+ delete record.toolCallIdStyle;
74
+ }
75
+ if (RAW_SYSTEM_SENTINEL in record) {
76
+ delete record[RAW_SYSTEM_SENTINEL];
77
+ }
78
+ if (record.extraFields && typeof record.extraFields === 'object') {
79
+ prunePrivateExtraFields(record.extraFields);
80
+ if (!Object.keys(record.extraFields).length) {
81
+ delete record.extraFields;
82
+ }
83
+ }
84
+ }
85
+ function prunePrivateExtraFields(target) {
86
+ for (const key of Object.keys(target)) {
87
+ const value = target[key];
88
+ if (typeof key === 'string' && key.startsWith('__rcc_')) {
89
+ delete target[key];
90
+ continue;
91
+ }
92
+ if (value && typeof value === 'object') {
93
+ prunePrivateExtraFields(value);
94
+ if (!Object.keys(value).length) {
95
+ delete target[key];
96
+ }
97
+ }
73
98
  }
74
99
  }
100
+ const RAW_SYSTEM_SENTINEL = '__rcc_raw_system';
75
101
  export function sanitizeResponsesFunctionName(rawName) {
76
102
  if (typeof rawName !== 'string') {
77
103
  return undefined;
@@ -0,0 +1,9 @@
1
+ export interface SnapshotHookOptions {
2
+ endpoint: string;
3
+ stage: string;
4
+ requestId: string;
5
+ data: unknown;
6
+ verbosity?: 'minimal' | 'verbose';
7
+ channel?: string;
8
+ }
9
+ export declare function writeSnapshotViaHooks(options: SnapshotHookOptions): Promise<void>;
@@ -1,7 +1,61 @@
1
- /**
2
- * Hub pipeline no longer exposes the legacy hooks runtime.
3
- * Keep a tiny shim so existing callers can continue awaiting the same API surface.
4
- */
5
- export async function writeSnapshotViaHooks(_options) {
6
- return;
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const DEFAULT_SNAPSHOT_ROOT = path.join(os.homedir(), '.routecodex', 'codex-samples');
5
+ function resolveSnapshotRoot() {
6
+ const envOverride = process.env.RCC_SNAPSHOT_DIR ||
7
+ process.env.ROUTECODEX_SNAPSHOT_DIR;
8
+ if (envOverride && envOverride.trim()) {
9
+ return path.resolve(envOverride.trim());
10
+ }
11
+ return DEFAULT_SNAPSHOT_ROOT;
12
+ }
13
+ function resolveSnapshotFolder(endpoint) {
14
+ const lowered = (endpoint || '').toLowerCase();
15
+ if (lowered.includes('/responses')) {
16
+ return 'openai-responses';
17
+ }
18
+ if (lowered.includes('/messages')) {
19
+ return 'anthropic-messages';
20
+ }
21
+ return 'openai-chat';
22
+ }
23
+ function sanitizeToken(value, fallback) {
24
+ if (typeof value !== 'string') {
25
+ return fallback;
26
+ }
27
+ const trimmed = value.trim();
28
+ if (!trimmed) {
29
+ return fallback;
30
+ }
31
+ return trimmed.replace(/[^A-Za-z0-9_.-]/g, '_') || fallback;
32
+ }
33
+ function channelSuffix(channel) {
34
+ if (!channel) {
35
+ return '';
36
+ }
37
+ const token = sanitizeToken(channel, '');
38
+ return token ? `_${token}` : '';
39
+ }
40
+ async function writeSnapshotFile(options) {
41
+ const root = resolveSnapshotRoot();
42
+ const folder = resolveSnapshotFolder(options.endpoint);
43
+ const dir = path.join(root, folder);
44
+ const stageToken = sanitizeToken(options.stage, 'snapshot');
45
+ const requestToken = sanitizeToken(options.requestId, `req_${Date.now()}`);
46
+ const filePath = path.join(dir, `${requestToken}_${stageToken}${channelSuffix(options.channel)}.json`);
47
+ await fs.mkdir(dir, { recursive: true });
48
+ const spacing = options.verbosity === 'minimal' ? undefined : 2;
49
+ const payload = spacing !== undefined
50
+ ? JSON.stringify(options.data, null, spacing)
51
+ : JSON.stringify(options.data);
52
+ await fs.writeFile(filePath, payload, 'utf-8');
53
+ }
54
+ export async function writeSnapshotViaHooks(options) {
55
+ try {
56
+ await writeSnapshotFile(options);
57
+ }
58
+ catch {
59
+ // snapshot writes must never block callers
60
+ }
7
61
  }
@@ -0,0 +1,16 @@
1
+ interface SnapshotPayload {
2
+ stage: string;
3
+ requestId: string;
4
+ endpoint?: string;
5
+ data: unknown;
6
+ folderHint?: string;
7
+ }
8
+ export declare function shouldRecordSnapshots(): boolean;
9
+ export declare function recordSnapshot(options: SnapshotPayload): Promise<void>;
10
+ export type SnapshotWriter = (stage: string, payload: unknown) => void;
11
+ export declare function createSnapshotWriter(opts: {
12
+ requestId: string;
13
+ endpoint?: string;
14
+ folderHint?: string;
15
+ }): SnapshotWriter | undefined;
16
+ export {};
@@ -0,0 +1,84 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ import { writeSnapshotViaHooks } from './snapshot-hooks.js';
5
+ const SNAPSHOT_BASE = path.join(os.homedir(), '.routecodex', 'golden_samples');
6
+ function mapEndpointToFolder(endpoint, hint) {
7
+ if (hint)
8
+ return hint;
9
+ const ep = String(endpoint || '').toLowerCase();
10
+ if (ep.includes('/responses'))
11
+ return 'openai-responses';
12
+ if (ep.includes('/messages'))
13
+ return 'anthropic-messages';
14
+ if (ep.includes('/gemini'))
15
+ return 'gemini-chat';
16
+ return 'openai-chat';
17
+ }
18
+ async function ensureDir(dir) {
19
+ try {
20
+ await fs.mkdir(dir, { recursive: true });
21
+ }
22
+ catch {
23
+ // ignore fs errors
24
+ }
25
+ }
26
+ function sanitize(value) {
27
+ return value.replace(/[^\w.-]/g, '_');
28
+ }
29
+ export function shouldRecordSnapshots() {
30
+ const flag = process.env.ROUTECODEX_HUB_SNAPSHOTS;
31
+ if (flag && flag.trim() === '0') {
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+ export async function recordSnapshot(options) {
37
+ if (!shouldRecordSnapshots())
38
+ return;
39
+ const endpoint = options.endpoint || '/v1/chat/completions';
40
+ const folder = mapEndpointToFolder(endpoint, options.folderHint);
41
+ const dir = path.join(SNAPSHOT_BASE, folder);
42
+ try {
43
+ await ensureDir(dir);
44
+ const safeStage = sanitize(options.stage);
45
+ const safeRequestId = sanitize(options.requestId);
46
+ const file = path.join(dir, `${safeRequestId}_${safeStage}.json`);
47
+ const payload = {
48
+ meta: {
49
+ stage: options.stage,
50
+ timestamp: Date.now(),
51
+ endpoint
52
+ },
53
+ body: options.data
54
+ };
55
+ await fs.writeFile(file, JSON.stringify(payload, null, 2), 'utf-8');
56
+ }
57
+ catch (error) {
58
+ console.warn('[snapshot-utils] failed to write snapshot', error);
59
+ }
60
+ void writeSnapshotViaHooks({
61
+ endpoint,
62
+ stage: options.stage,
63
+ requestId: options.requestId,
64
+ data: options.data,
65
+ verbosity: 'verbose'
66
+ }).catch(() => {
67
+ // ignore hook errors
68
+ });
69
+ }
70
+ export function createSnapshotWriter(opts) {
71
+ if (!shouldRecordSnapshots()) {
72
+ return undefined;
73
+ }
74
+ const endpoint = opts.endpoint || '/v1/chat/completions';
75
+ return (stage, payload) => {
76
+ void recordSnapshot({
77
+ stage,
78
+ requestId: opts.requestId,
79
+ endpoint,
80
+ folderHint: opts.folderHint,
81
+ data: payload
82
+ });
83
+ };
84
+ }