@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.
- package/dist/conversion/codecs/anthropic-openai-codec.js +28 -2
- package/dist/conversion/codecs/gemini-openai-codec.js +23 -0
- package/dist/conversion/codecs/responses-openai-codec.js +8 -1
- package/dist/conversion/hub/node-support.js +14 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +66 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +284 -193
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.d.ts +11 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +17 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.d.ts +5 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js +17 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.d.ts +19 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +269 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.d.ts +18 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +141 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +11 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +29 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +15 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +18 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +63 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.d.ts +11 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +12 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +13 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +43 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +22 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +19 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.d.ts +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +19 -0
- package/dist/conversion/hub/pipeline/stages/utils.d.ts +2 -0
- package/dist/conversion/hub/pipeline/stages/utils.js +11 -0
- package/dist/conversion/hub/pipeline/target-utils.d.ts +5 -0
- package/dist/conversion/hub/pipeline/target-utils.js +87 -0
- package/dist/conversion/hub/process/chat-process.js +11 -11
- package/dist/conversion/hub/response/provider-response.js +69 -122
- package/dist/conversion/hub/response/response-mappers.d.ts +19 -0
- package/dist/conversion/hub/response/response-mappers.js +22 -2
- package/dist/conversion/hub/response/response-runtime.d.ts +8 -0
- package/dist/conversion/hub/response/response-runtime.js +239 -6
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +8 -0
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +119 -59
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +74 -13
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +0 -9
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +16 -13
- package/dist/conversion/hub/snapshot-recorder.d.ts +13 -0
- package/dist/conversion/hub/snapshot-recorder.js +90 -50
- package/dist/conversion/hub/standardized-bridge.js +44 -30
- package/dist/conversion/hub/types/chat-envelope.d.ts +68 -0
- package/dist/conversion/hub/types/standardized.d.ts +97 -0
- package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +29 -2
- package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +68 -1
- package/dist/conversion/responses/responses-openai-bridge.d.ts +6 -1
- package/dist/conversion/responses/responses-openai-bridge.js +132 -6
- package/dist/conversion/shared/anthropic-message-utils.d.ts +9 -1
- package/dist/conversion/shared/anthropic-message-utils.js +334 -14
- package/dist/conversion/shared/bridge-actions.js +267 -40
- package/dist/conversion/shared/bridge-message-utils.js +54 -8
- package/dist/conversion/shared/bridge-policies.js +29 -4
- package/dist/conversion/shared/chat-envelope-validator.d.ts +8 -0
- package/dist/conversion/shared/chat-envelope-validator.js +128 -0
- package/dist/conversion/shared/chat-request-filters.js +108 -25
- package/dist/conversion/shared/mcp-injection.js +41 -20
- package/dist/conversion/shared/openai-finalizer.d.ts +11 -0
- package/dist/conversion/shared/openai-finalizer.js +73 -0
- package/dist/conversion/shared/openai-message-normalize.js +32 -31
- package/dist/conversion/shared/reasoning-normalizer.d.ts +1 -0
- package/dist/conversion/shared/reasoning-normalizer.js +50 -18
- package/dist/conversion/shared/responses-output-builder.d.ts +1 -1
- package/dist/conversion/shared/responses-output-builder.js +76 -25
- package/dist/conversion/shared/responses-reasoning-registry.d.ts +8 -0
- package/dist/conversion/shared/responses-reasoning-registry.js +61 -0
- package/dist/conversion/shared/responses-response-utils.js +32 -2
- package/dist/conversion/shared/responses-tool-utils.js +28 -2
- package/dist/conversion/shared/snapshot-hooks.d.ts +9 -0
- package/dist/conversion/shared/snapshot-hooks.js +60 -6
- package/dist/conversion/shared/snapshot-utils.d.ts +16 -0
- package/dist/conversion/shared/snapshot-utils.js +84 -0
- package/dist/conversion/shared/tool-filter-pipeline.js +45 -5
- package/dist/conversion/shared/tool-governor.js +5 -0
- package/dist/conversion/shared/tool-mapping.js +13 -2
- package/dist/filters/special/request-tool-choice-policy.js +3 -1
- package/dist/filters/special/request-tool-list-filter.d.ts +11 -0
- package/dist/filters/special/request-tool-list-filter.js +20 -7
- package/dist/sse/shared/responses-output-normalizer.js +5 -4
- 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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 {
|
|
2
|
-
import
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
stage,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
102
|
+
function cloneJson(payload) {
|
|
49
103
|
try {
|
|
50
|
-
|
|
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
|
|
67
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
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:
|
|
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
|
+
}
|