@jsonstudio/llms 0.4.6 → 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 (91) 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-mapping.js +13 -2
  87. package/dist/filters/special/request-tool-choice-policy.js +3 -1
  88. package/dist/filters/special/request-tool-list-filter.d.ts +11 -0
  89. package/dist/filters/special/request-tool-list-filter.js +20 -7
  90. package/dist/sse/shared/responses-output-normalizer.js +5 -4
  91. package/package.json +1 -1
@@ -25,7 +25,9 @@ const KNOWN_TOP_LEVEL_FIELDS = new Set([
25
25
  'messages',
26
26
  'tools',
27
27
  'tool_outputs',
28
- ...CHAT_PARAMETER_KEYS
28
+ ...CHAT_PARAMETER_KEYS,
29
+ 'stageExpectations',
30
+ 'stages'
29
31
  ]);
30
32
  function flattenSystemContent(content) {
31
33
  if (typeof content === 'string')
@@ -56,6 +58,37 @@ function normalizeToolContent(content) {
56
58
  return String(content ?? '');
57
59
  }
58
60
  }
61
+ function recordToolCallIssues(message, messageIndex, missing) {
62
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : undefined;
63
+ if (!toolCalls?.length)
64
+ return;
65
+ toolCalls.forEach((entry, callIndex) => {
66
+ if (!isJsonObject(entry)) {
67
+ missing.push({
68
+ path: `messages[${messageIndex}].tool_calls[${callIndex}]`,
69
+ reason: 'invalid_tool_call_entry',
70
+ originalValue: jsonClone(entry)
71
+ });
72
+ return;
73
+ }
74
+ const fnBlock = entry.function;
75
+ if (!isJsonObject(fnBlock)) {
76
+ missing.push({
77
+ path: `messages[${messageIndex}].tool_calls[${callIndex}].function`,
78
+ reason: 'missing_tool_function',
79
+ originalValue: jsonClone(fnBlock)
80
+ });
81
+ return;
82
+ }
83
+ const fnName = fnBlock.name;
84
+ if (typeof fnName !== 'string' || !fnName.trim().length) {
85
+ missing.push({
86
+ path: `messages[${messageIndex}].tool_calls[${callIndex}].function.name`,
87
+ reason: 'missing_tool_name'
88
+ });
89
+ }
90
+ });
91
+ }
59
92
  function collectSystemRawBlocks(raw) {
60
93
  if (!Array.isArray(raw))
61
94
  return undefined;
@@ -120,7 +153,8 @@ function normalizeChatMessages(raw) {
120
153
  const chatMessage = value;
121
154
  if (roleValue !== 'system' && roleValue !== 'tool') {
122
155
  const normalizedContent = normalizeChatMessageContent(chatMessage.content);
123
- if (normalizedContent.contentText !== undefined) {
156
+ const shouldOverwriteContent = !Array.isArray(chatMessage.content);
157
+ if (shouldOverwriteContent && normalizedContent.contentText !== undefined) {
124
158
  chatMessage.content = normalizedContent.contentText;
125
159
  }
126
160
  if (typeof normalizedContent.reasoningText === 'string' && normalizedContent.reasoningText.trim().length) {
@@ -128,6 +162,10 @@ function normalizeChatMessages(raw) {
128
162
  }
129
163
  }
130
164
  norm.messages.push(chatMessage);
165
+ const toolCallCandidate = value.tool_calls;
166
+ if (Array.isArray(toolCallCandidate) && toolCallCandidate.length) {
167
+ recordToolCallIssues(value, index, norm.missingFields);
168
+ }
131
169
  if (roleValue === 'system') {
132
170
  const segment = flattenSystemContent(chatMessage.content);
133
171
  if (segment.trim().length) {
@@ -197,6 +235,29 @@ function extractParameters(body) {
197
235
  }
198
236
  return Object.keys(params).length ? params : undefined;
199
237
  }
238
+ function collectExtraFields(body) {
239
+ const extras = {};
240
+ for (const [key, value] of Object.entries(body)) {
241
+ if (KNOWN_TOP_LEVEL_FIELDS.has(key)) {
242
+ continue;
243
+ }
244
+ if (value !== undefined) {
245
+ extras[key] = jsonClone(value);
246
+ }
247
+ }
248
+ return Object.keys(extras).length ? extras : undefined;
249
+ }
250
+ function applyExtraFields(body, metadata) {
251
+ if (!metadata || !metadata.extraFields || !isJsonObject(metadata.extraFields)) {
252
+ return;
253
+ }
254
+ for (const [key, value] of Object.entries(metadata.extraFields)) {
255
+ if (body[key] !== undefined) {
256
+ continue;
257
+ }
258
+ body[key] = jsonClone(value);
259
+ }
260
+ }
200
261
  export class ChatSemanticMapper {
201
262
  async toChat(format, ctx) {
202
263
  const payload = (format.payload ?? {});
@@ -219,6 +280,10 @@ export class ChatSemanticMapper {
219
280
  if (normalized.missingFields.length) {
220
281
  metadata.missingFields = normalized.missingFields;
221
282
  }
283
+ const extraFields = collectExtraFields(payload);
284
+ if (extraFields) {
285
+ metadata.extraFields = extraFields;
286
+ }
222
287
  try {
223
288
  const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-chat' });
224
289
  const actions = resolvePolicyActions(bridgePolicy, 'request_inbound');
@@ -258,6 +323,7 @@ export class ChatSemanticMapper {
258
323
  tools: chat.tools ?? (chat.metadata?.toolsFieldPresent ? [] : undefined),
259
324
  ...(chat.parameters || {})
260
325
  };
326
+ applyExtraFields(payload, chat.metadata);
261
327
  try {
262
328
  const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-chat' });
263
329
  const actions = resolvePolicyActions(bridgePolicy, 'request_outbound');
@@ -288,17 +354,12 @@ export class ChatSemanticMapper {
288
354
  catch {
289
355
  // ignore policy failures
290
356
  }
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
- }
357
+ // Do not forward tool_outputs to provider wire formats. OpenAI Chat
358
+ // endpoints expect tool results to appear as tool role messages, and
359
+ // sending the legacy top-level field causes upstream HTTP 400 responses.
360
+ // Concrete translation happens earlier when responses input is unfolded
361
+ // into ChatEnvelope.messages, so the provider request only needs the
362
+ // canonical message list.
302
363
  if (payload.max_tokens === undefined && typeof payload.max_output_tokens === 'number') {
303
364
  payload.max_tokens = payload.max_output_tokens;
304
365
  delete payload.max_output_tokens;
@@ -192,15 +192,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
192
192
  request.metadata = request.metadata ?? {};
193
193
  request.metadata.__rcc_stream = chat.parameters.stream;
194
194
  }
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
195
  if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
205
196
  request.metadata = request.metadata ?? {};
206
197
  request.metadata.__rcc_tools_field_present = '1';
@@ -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 { 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 ?? {}) };
@@ -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 ?? {}),
@@ -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
  };
@@ -0,0 +1,68 @@
1
+ export type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
2
+ import type { JsonObject, JsonValue } from './json.js';
3
+ export interface ChatMessageContentPart {
4
+ type: string;
5
+ text?: string;
6
+ [key: string]: JsonValue;
7
+ }
8
+ export interface ChatToolCall {
9
+ id: string;
10
+ type: 'function';
11
+ function: {
12
+ name: string;
13
+ arguments: string;
14
+ };
15
+ [key: string]: JsonValue;
16
+ }
17
+ export interface ChatMessage {
18
+ role: ChatRole;
19
+ content?: string | ChatMessageContentPart[] | null;
20
+ tool_calls?: ChatToolCall[];
21
+ name?: string;
22
+ [key: string]: JsonValue;
23
+ }
24
+ export interface ChatToolDefinition {
25
+ type: 'function' | string;
26
+ function: {
27
+ name: string;
28
+ description?: string;
29
+ parameters?: JsonValue;
30
+ strict?: boolean;
31
+ };
32
+ [key: string]: JsonValue;
33
+ }
34
+ export interface ChatToolOutput {
35
+ tool_call_id: string;
36
+ content: string;
37
+ name?: string;
38
+ [key: string]: JsonValue;
39
+ }
40
+ export interface MissingField extends JsonObject {
41
+ path: string;
42
+ reason: string;
43
+ originalValue?: JsonValue;
44
+ }
45
+ export interface AdapterContext {
46
+ requestId: string;
47
+ entryEndpoint: string;
48
+ providerProtocol: string;
49
+ providerId?: string;
50
+ routeId?: string;
51
+ profileId?: string;
52
+ streamingHint?: 'auto' | 'force' | 'disable';
53
+ originalModelId?: string;
54
+ clientModelId?: string;
55
+ toolCallIdStyle?: 'fc' | 'preserve';
56
+ [key: string]: JsonValue;
57
+ }
58
+ export interface ChatEnvelope {
59
+ messages: ChatMessage[];
60
+ tools?: ChatToolDefinition[];
61
+ toolOutputs?: ChatToolOutput[];
62
+ parameters?: JsonObject;
63
+ metadata: {
64
+ context: AdapterContext;
65
+ missingFields?: MissingField[];
66
+ [key: string]: JsonValue;
67
+ };
68
+ }
@@ -0,0 +1,97 @@
1
+ import type { ChatMessageContentPart } from './chat-envelope.js';
2
+ import type { JsonObject } from './json.js';
3
+ export type ToolChoice = 'none' | 'auto' | 'required' | {
4
+ type: 'function';
5
+ function: {
6
+ name: string;
7
+ };
8
+ } | Record<string, unknown>;
9
+ export interface StandardizedTool {
10
+ type: 'function';
11
+ function: {
12
+ name: string;
13
+ description?: string;
14
+ parameters: {
15
+ type?: string | string[];
16
+ properties?: Record<string, unknown>;
17
+ required?: string[];
18
+ additionalProperties?: boolean;
19
+ [key: string]: unknown;
20
+ };
21
+ strict?: boolean;
22
+ };
23
+ }
24
+ export interface ToolCall {
25
+ id: string;
26
+ type: 'function';
27
+ function: {
28
+ name: string;
29
+ arguments: string;
30
+ };
31
+ }
32
+ export interface ToolCallResult {
33
+ tool_call_id: string;
34
+ status: 'success' | 'error' | 'timeout' | 'pending';
35
+ result?: JsonObject | JsonObject[] | string | number | boolean | null;
36
+ error?: string;
37
+ executionTime?: number;
38
+ }
39
+ export type StandardizedMessageContent = string | ChatMessageContentPart[] | null;
40
+ export interface StandardizedMessage {
41
+ role: 'system' | 'user' | 'assistant' | 'tool';
42
+ content: StandardizedMessageContent;
43
+ tool_calls?: ToolCall[];
44
+ tool_call_id?: string;
45
+ name?: string;
46
+ metadata?: Record<string, unknown>;
47
+ }
48
+ export interface StandardizedParameters {
49
+ temperature?: number;
50
+ max_tokens?: number;
51
+ top_p?: number;
52
+ frequency_penalty?: number;
53
+ presence_penalty?: number;
54
+ stop?: string | string[];
55
+ stream?: boolean;
56
+ [key: string]: unknown;
57
+ }
58
+ export interface StandardizedMetadata {
59
+ originalEndpoint: string;
60
+ capturedContext?: Record<string, unknown>;
61
+ requestId?: string;
62
+ stream?: boolean;
63
+ toolChoice?: ToolChoice;
64
+ providerKey?: string;
65
+ providerType?: string;
66
+ processMode?: 'chat' | 'passthrough';
67
+ routeHint?: string;
68
+ [key: string]: unknown;
69
+ }
70
+ export interface StandardizedRequest {
71
+ model: string;
72
+ messages: StandardizedMessage[];
73
+ tools?: StandardizedTool[];
74
+ parameters: StandardizedParameters;
75
+ metadata: StandardizedMetadata;
76
+ }
77
+ export interface ProcessedRequest extends StandardizedRequest {
78
+ processed: {
79
+ timestamp: number;
80
+ appliedRules: string[];
81
+ status: 'success' | 'partial' | 'failed';
82
+ };
83
+ processingMetadata: {
84
+ toolCalls?: ToolCallResult[];
85
+ streaming?: {
86
+ enabled: boolean;
87
+ chunkCount?: number;
88
+ totalTokens?: number;
89
+ };
90
+ context?: {
91
+ systemPrompt?: string;
92
+ conversationHistory?: string[];
93
+ relevantContext?: Record<string, unknown>;
94
+ };
95
+ passthrough?: boolean;
96
+ };
97
+ }