@jsonstudio/llms 0.6.3214 → 0.6.3238
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/hub/operation-table/semantic-mappers/archive/chat-mapper.archive.d.ts +8 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/archive/chat-mapper.archive.js +404 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +5 -384
- package/dist/conversion/hub/snapshot-recorder.js +2 -91
- package/dist/conversion/hub/tool-governance/engine.d.ts +1 -1
- package/dist/conversion/hub/tool-governance/engine.js +17 -127
- package/dist/native/router_hotpath_napi.node +0 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.d.ts +8 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.js +48 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-semantic-mappers.d.ts +2 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-semantic-mappers.js +83 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +5 -0
- package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SemanticMapper } from '../../../format-adapters/index.js';
|
|
2
|
+
import type { AdapterContext, ChatEnvelope } from '../../../types/chat-envelope.js';
|
|
3
|
+
import type { FormatEnvelope } from '../../../types/format-envelope.js';
|
|
4
|
+
export declare function maybeAugmentApplyPatchErrorContent(content: string, toolName?: string): string;
|
|
5
|
+
export declare class ChatSemanticMapper implements SemanticMapper {
|
|
6
|
+
toChat(format: FormatEnvelope, ctx: AdapterContext): Promise<ChatEnvelope>;
|
|
7
|
+
fromChat(chat: ChatEnvelope, ctx: AdapterContext): Promise<FormatEnvelope>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { isJsonObject, jsonClone } from '../../../types/json.js';
|
|
2
|
+
import { normalizeChatMessageContentWithNative, normalizeOpenaiChatMessagesWithNative } from '../../../../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
|
|
3
|
+
import { ensureProtocolState } from '../../../../protocol-state.js';
|
|
4
|
+
import { mapReqInboundBridgeToolsToChatWithNative } from '../../../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js';
|
|
5
|
+
const CHAT_PARAMETER_KEYS = [
|
|
6
|
+
'model',
|
|
7
|
+
'temperature',
|
|
8
|
+
'top_p',
|
|
9
|
+
'top_k',
|
|
10
|
+
'max_tokens',
|
|
11
|
+
'frequency_penalty',
|
|
12
|
+
'presence_penalty',
|
|
13
|
+
'logit_bias',
|
|
14
|
+
'response_format',
|
|
15
|
+
'parallel_tool_calls',
|
|
16
|
+
'tool_choice',
|
|
17
|
+
'seed',
|
|
18
|
+
'user',
|
|
19
|
+
'metadata',
|
|
20
|
+
'stop',
|
|
21
|
+
'stop_sequences',
|
|
22
|
+
'stream'
|
|
23
|
+
];
|
|
24
|
+
const KNOWN_TOP_LEVEL_FIELDS = new Set([
|
|
25
|
+
'messages',
|
|
26
|
+
'tools',
|
|
27
|
+
'tool_outputs',
|
|
28
|
+
...CHAT_PARAMETER_KEYS,
|
|
29
|
+
'stageExpectations',
|
|
30
|
+
'stages'
|
|
31
|
+
]);
|
|
32
|
+
function flattenSystemContent(content) {
|
|
33
|
+
if (typeof content === 'string')
|
|
34
|
+
return content;
|
|
35
|
+
if (Array.isArray(content)) {
|
|
36
|
+
return content.map(flattenSystemContent).filter(Boolean).join('\n');
|
|
37
|
+
}
|
|
38
|
+
if (content && typeof content === 'object') {
|
|
39
|
+
const obj = content;
|
|
40
|
+
if (typeof obj.text === 'string')
|
|
41
|
+
return obj.text;
|
|
42
|
+
if (typeof obj.content === 'string')
|
|
43
|
+
return obj.content;
|
|
44
|
+
if (Array.isArray(obj.content))
|
|
45
|
+
return obj.content.map(flattenSystemContent).join('\n');
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
function normalizeToolContent(content) {
|
|
50
|
+
if (typeof content === 'string')
|
|
51
|
+
return content;
|
|
52
|
+
if (content === null || content === undefined)
|
|
53
|
+
return '';
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(content);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return String(content ?? '');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function maybeAugmentRouteCodexApplyPatchPrecheck(content) {
|
|
62
|
+
if (!content || typeof content !== 'string') {
|
|
63
|
+
return content;
|
|
64
|
+
}
|
|
65
|
+
if (content.includes('[RouteCodex precheck]')) {
|
|
66
|
+
return content;
|
|
67
|
+
}
|
|
68
|
+
const lower = content.toLowerCase();
|
|
69
|
+
if (!lower.includes('failed to parse function arguments')) {
|
|
70
|
+
return content;
|
|
71
|
+
}
|
|
72
|
+
if (content.includes('missing field `input`')) {
|
|
73
|
+
return `${content}\n\n[RouteCodex precheck] apply_patch 参数解析失败:缺少字段 "input"。当前 RouteCodex 期望 { input, patch } 形态,并且两个字段都应包含完整统一 diff 文本。`;
|
|
74
|
+
}
|
|
75
|
+
if (content.includes('invalid type: map, expected a string')) {
|
|
76
|
+
return `${content}\n\n[RouteCodex precheck] apply_patch 参数类型错误:检测到 JSON 对象(map),但客户端期望字符串。请先对参数做 JSON.stringify 再写入 arguments,或直接提供 { patch: "<统一 diff>" } 形式。`;
|
|
77
|
+
}
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
export function maybeAugmentApplyPatchErrorContent(content, toolName) {
|
|
81
|
+
if (!content)
|
|
82
|
+
return content;
|
|
83
|
+
const lower = content.toLowerCase();
|
|
84
|
+
const isApplyPatch = (typeof toolName === 'string' && toolName.trim() === 'apply_patch') ||
|
|
85
|
+
lower.includes('apply_patch verification failed');
|
|
86
|
+
if (!isApplyPatch) {
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
// 避免重复追加提示。
|
|
90
|
+
if (content.includes('[apply_patch hint]')) {
|
|
91
|
+
return content;
|
|
92
|
+
}
|
|
93
|
+
const hint = '\n\n[apply_patch hint] 在使用 apply_patch 之前,请先读取目标文件的最新内容,并基于该内容生成补丁;同时确保补丁格式符合工具规范(统一补丁格式或结构化参数),避免上下文不匹配或语法错误。';
|
|
94
|
+
return content + hint;
|
|
95
|
+
}
|
|
96
|
+
function recordToolCallIssues(message, messageIndex, missing) {
|
|
97
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : undefined;
|
|
98
|
+
if (!toolCalls?.length)
|
|
99
|
+
return;
|
|
100
|
+
toolCalls.forEach((entry, callIndex) => {
|
|
101
|
+
if (!isJsonObject(entry)) {
|
|
102
|
+
missing.push({
|
|
103
|
+
path: `messages[${messageIndex}].tool_calls[${callIndex}]`,
|
|
104
|
+
reason: 'invalid_tool_call_entry',
|
|
105
|
+
originalValue: jsonClone(entry)
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const fnBlock = entry.function;
|
|
110
|
+
if (!isJsonObject(fnBlock)) {
|
|
111
|
+
missing.push({
|
|
112
|
+
path: `messages[${messageIndex}].tool_calls[${callIndex}].function`,
|
|
113
|
+
reason: 'missing_tool_function',
|
|
114
|
+
originalValue: jsonClone(fnBlock)
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const fnName = fnBlock.name;
|
|
119
|
+
if (typeof fnName !== 'string' || !fnName.trim().length) {
|
|
120
|
+
missing.push({
|
|
121
|
+
path: `messages[${messageIndex}].tool_calls[${callIndex}].function.name`,
|
|
122
|
+
reason: 'missing_tool_name'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function collectSystemRawBlocks(raw) {
|
|
128
|
+
if (!Array.isArray(raw))
|
|
129
|
+
return undefined;
|
|
130
|
+
const blocks = [];
|
|
131
|
+
raw.forEach((entry) => {
|
|
132
|
+
if (!isJsonObject(entry))
|
|
133
|
+
return;
|
|
134
|
+
if (String(entry.role ?? '').toLowerCase() !== 'system')
|
|
135
|
+
return;
|
|
136
|
+
blocks.push(jsonClone(entry));
|
|
137
|
+
});
|
|
138
|
+
return blocks.length ? blocks : undefined;
|
|
139
|
+
}
|
|
140
|
+
function normalizeChatMessages(raw) {
|
|
141
|
+
const norm = {
|
|
142
|
+
messages: [],
|
|
143
|
+
systemSegments: [],
|
|
144
|
+
toolOutputs: [],
|
|
145
|
+
missingFields: []
|
|
146
|
+
};
|
|
147
|
+
if (raw === undefined) {
|
|
148
|
+
norm.missingFields.push({ path: 'messages', reason: 'absent' });
|
|
149
|
+
return norm;
|
|
150
|
+
}
|
|
151
|
+
const normalizedRaw = Array.isArray(raw)
|
|
152
|
+
? normalizeOpenaiChatMessagesWithNative(raw)
|
|
153
|
+
: raw;
|
|
154
|
+
if (!Array.isArray(normalizedRaw)) {
|
|
155
|
+
norm.missingFields.push({ path: 'messages', reason: 'invalid_type', originalValue: jsonClone(raw) });
|
|
156
|
+
return norm;
|
|
157
|
+
}
|
|
158
|
+
normalizedRaw.forEach((value, index) => {
|
|
159
|
+
if (!isJsonObject(value)) {
|
|
160
|
+
norm.missingFields.push({ path: `messages[${index}]`, reason: 'invalid_entry', originalValue: jsonClone(value) });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const roleValue = value.role;
|
|
164
|
+
if (typeof roleValue !== 'string') {
|
|
165
|
+
norm.missingFields.push({ path: `messages[${index}].role`, reason: 'missing_role' });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const chatMessage = value;
|
|
169
|
+
if (roleValue !== 'system' && roleValue !== 'tool') {
|
|
170
|
+
const normalizedContent = normalizeChatMessageContentWithNative(chatMessage.content);
|
|
171
|
+
const shouldOverwriteContent = !Array.isArray(chatMessage.content);
|
|
172
|
+
if (shouldOverwriteContent && normalizedContent.contentText !== undefined) {
|
|
173
|
+
chatMessage.content = normalizedContent.contentText;
|
|
174
|
+
}
|
|
175
|
+
if (typeof normalizedContent.reasoningText === 'string' && normalizedContent.reasoningText.trim().length) {
|
|
176
|
+
chatMessage.reasoning_content = normalizedContent.reasoningText.trim();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
norm.messages.push(chatMessage);
|
|
180
|
+
const toolCallCandidate = value.tool_calls;
|
|
181
|
+
if (Array.isArray(toolCallCandidate) && toolCallCandidate.length) {
|
|
182
|
+
recordToolCallIssues(value, index, norm.missingFields);
|
|
183
|
+
}
|
|
184
|
+
if (roleValue === 'system') {
|
|
185
|
+
const segment = flattenSystemContent(chatMessage.content);
|
|
186
|
+
if (segment.trim().length) {
|
|
187
|
+
norm.systemSegments.push(segment);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (roleValue === 'tool') {
|
|
192
|
+
const rawCallId = (value.tool_call_id ?? value.call_id ?? value.id);
|
|
193
|
+
const toolCallId = typeof rawCallId === 'string' && rawCallId.trim().length ? rawCallId.trim() : undefined;
|
|
194
|
+
if (!toolCallId) {
|
|
195
|
+
norm.missingFields.push({ path: `messages[${index}].tool_call_id`, reason: 'missing_tool_call_id' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const nameValue = typeof value.name === 'string' && value.name.trim().length ? value.name : undefined;
|
|
199
|
+
const normalizedToolOutput = normalizeToolContent(value.content ?? value.output);
|
|
200
|
+
const routeCodexPrechecked = maybeAugmentRouteCodexApplyPatchPrecheck(normalizedToolOutput);
|
|
201
|
+
if (routeCodexPrechecked !== normalizedToolOutput) {
|
|
202
|
+
// Keep tool role message content aligned with outbound provider requests (e.g. Chat→Responses),
|
|
203
|
+
// while avoiding double-injection.
|
|
204
|
+
if (typeof chatMessage.content === 'string' || chatMessage.content === undefined || chatMessage.content === null) {
|
|
205
|
+
chatMessage.content = routeCodexPrechecked;
|
|
206
|
+
}
|
|
207
|
+
else if (typeof chatMessage.output === 'string') {
|
|
208
|
+
chatMessage.output = routeCodexPrechecked;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const outputEntry = {
|
|
212
|
+
tool_call_id: toolCallId,
|
|
213
|
+
content: routeCodexPrechecked,
|
|
214
|
+
name: nameValue
|
|
215
|
+
};
|
|
216
|
+
outputEntry.content = maybeAugmentApplyPatchErrorContent(outputEntry.content, outputEntry.name);
|
|
217
|
+
norm.toolOutputs.push(outputEntry);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return norm;
|
|
221
|
+
}
|
|
222
|
+
function normalizeStandaloneToolOutputs(raw, missing) {
|
|
223
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
224
|
+
return [];
|
|
225
|
+
const outputs = [];
|
|
226
|
+
raw.forEach((entry, index) => {
|
|
227
|
+
if (!isJsonObject(entry)) {
|
|
228
|
+
missing.push({ path: `tool_outputs[${index}]`, reason: 'invalid_entry', originalValue: jsonClone(entry) });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const rawCallId = entry.tool_call_id ?? entry.call_id ?? entry.id;
|
|
232
|
+
const toolCallId = typeof rawCallId === 'string' && rawCallId.trim().length ? rawCallId.trim() : undefined;
|
|
233
|
+
if (!toolCallId) {
|
|
234
|
+
missing.push({ path: `tool_outputs[${index}].tool_call_id`, reason: 'missing_tool_call_id' });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const nameValue = typeof entry.name === 'string' && entry.name.trim().length ? entry.name : undefined;
|
|
238
|
+
const rawContent = normalizeToolContent(entry.content ?? entry.output);
|
|
239
|
+
const content = maybeAugmentApplyPatchErrorContent(rawContent, nameValue);
|
|
240
|
+
outputs.push({
|
|
241
|
+
tool_call_id: toolCallId,
|
|
242
|
+
content,
|
|
243
|
+
name: nameValue
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
return outputs;
|
|
247
|
+
}
|
|
248
|
+
function normalizeTools(raw, missing) {
|
|
249
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const tools = mapReqInboundBridgeToolsToChatWithNative(raw);
|
|
253
|
+
if (tools.length === 0) {
|
|
254
|
+
raw.forEach((entry, index) => {
|
|
255
|
+
missing.push({ path: `tools[${index}]`, reason: 'invalid_entry', originalValue: jsonClone(entry) });
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return tools.length ? tools : undefined;
|
|
259
|
+
}
|
|
260
|
+
function extractParameters(body) {
|
|
261
|
+
const params = {};
|
|
262
|
+
for (const key of CHAT_PARAMETER_KEYS) {
|
|
263
|
+
if (body[key] !== undefined) {
|
|
264
|
+
params[key] = body[key];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return Object.keys(params).length ? params : undefined;
|
|
268
|
+
}
|
|
269
|
+
function collectExtraFields(body) {
|
|
270
|
+
const extras = {};
|
|
271
|
+
for (const [key, value] of Object.entries(body)) {
|
|
272
|
+
if (KNOWN_TOP_LEVEL_FIELDS.has(key)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (value !== undefined) {
|
|
276
|
+
extras[key] = jsonClone(value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return Object.keys(extras).length ? extras : undefined;
|
|
280
|
+
}
|
|
281
|
+
function extractOpenAIExtraFieldsFromSemantics(semantics) {
|
|
282
|
+
if (!semantics || !semantics.providerExtras || !isJsonObject(semantics.providerExtras)) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
const openaiExtras = semantics.providerExtras.openaiChat;
|
|
286
|
+
if (!openaiExtras || !isJsonObject(openaiExtras)) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const stored = openaiExtras.extraFields;
|
|
290
|
+
if (!stored || !isJsonObject(stored)) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
return stored;
|
|
294
|
+
}
|
|
295
|
+
function hasExplicitEmptyToolsSemantics(semantics) {
|
|
296
|
+
if (!semantics || !semantics.tools || !isJsonObject(semantics.tools)) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const flag = semantics.tools.explicitEmpty;
|
|
300
|
+
return flag === true;
|
|
301
|
+
}
|
|
302
|
+
function buildOpenAISemantics(options) {
|
|
303
|
+
const semantics = {};
|
|
304
|
+
if (options.systemSegments && options.systemSegments.length) {
|
|
305
|
+
semantics.system = {
|
|
306
|
+
textBlocks: options.systemSegments.map((segment) => segment)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (options.extraFields && Object.keys(options.extraFields).length) {
|
|
310
|
+
semantics.providerExtras = {
|
|
311
|
+
openaiChat: {
|
|
312
|
+
extraFields: jsonClone(options.extraFields)
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (options.explicitEmptyTools) {
|
|
317
|
+
semantics.tools = {
|
|
318
|
+
explicitEmpty: true
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return Object.keys(semantics).length ? semantics : undefined;
|
|
322
|
+
}
|
|
323
|
+
function applyExtraFields(body, metadata, semantics) {
|
|
324
|
+
const sources = [];
|
|
325
|
+
const semanticsExtras = extractOpenAIExtraFieldsFromSemantics(semantics);
|
|
326
|
+
if (semanticsExtras) {
|
|
327
|
+
sources.push(semanticsExtras);
|
|
328
|
+
}
|
|
329
|
+
if (!sources.length) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
for (const source of sources) {
|
|
333
|
+
for (const [key, value] of Object.entries(source)) {
|
|
334
|
+
if (body[key] !== undefined) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
body[key] = jsonClone(value);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export class ChatSemanticMapper {
|
|
342
|
+
async toChat(format, ctx) {
|
|
343
|
+
const payload = (format.payload ?? {});
|
|
344
|
+
const normalized = normalizeChatMessages(payload.messages);
|
|
345
|
+
const topLevelOutputs = normalizeStandaloneToolOutputs(payload.tool_outputs, normalized.missingFields);
|
|
346
|
+
const toolOutputs = [...normalized.toolOutputs];
|
|
347
|
+
for (const entry of topLevelOutputs) {
|
|
348
|
+
if (!toolOutputs.find(item => item.tool_call_id === entry.tool_call_id)) {
|
|
349
|
+
toolOutputs.push(entry);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const metadata = { context: ctx };
|
|
353
|
+
const rawSystemBlocks = collectSystemRawBlocks(payload.messages);
|
|
354
|
+
if (rawSystemBlocks) {
|
|
355
|
+
const protocolState = ensureProtocolState(metadata, 'openai');
|
|
356
|
+
protocolState.systemMessages = jsonClone(rawSystemBlocks);
|
|
357
|
+
}
|
|
358
|
+
if (normalized.missingFields.length) {
|
|
359
|
+
metadata.missingFields = normalized.missingFields;
|
|
360
|
+
}
|
|
361
|
+
const extraFields = collectExtraFields(payload);
|
|
362
|
+
const explicitEmptyTools = Array.isArray(payload.tools) && payload.tools.length === 0;
|
|
363
|
+
const semantics = buildOpenAISemantics({
|
|
364
|
+
systemSegments: normalized.systemSegments,
|
|
365
|
+
extraFields,
|
|
366
|
+
explicitEmptyTools
|
|
367
|
+
});
|
|
368
|
+
return {
|
|
369
|
+
messages: normalized.messages,
|
|
370
|
+
tools: normalizeTools(payload.tools, normalized.missingFields),
|
|
371
|
+
toolOutputs: toolOutputs.length ? toolOutputs : undefined,
|
|
372
|
+
parameters: extractParameters(payload),
|
|
373
|
+
semantics,
|
|
374
|
+
metadata
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async fromChat(chat, ctx) {
|
|
378
|
+
const shouldEmitEmptyTools = hasExplicitEmptyToolsSemantics(chat.semantics);
|
|
379
|
+
const payload = {
|
|
380
|
+
messages: chat.messages,
|
|
381
|
+
tools: chat.tools ?? (shouldEmitEmptyTools ? [] : undefined),
|
|
382
|
+
...(chat.parameters || {})
|
|
383
|
+
};
|
|
384
|
+
applyExtraFields(payload, chat.metadata, chat.semantics);
|
|
385
|
+
// Do not forward tool_outputs to provider wire formats. OpenAI Chat
|
|
386
|
+
// endpoints expect tool results to appear as tool role messages, and
|
|
387
|
+
// sending the legacy top-level field causes upstream HTTP 400 responses.
|
|
388
|
+
// Concrete translation happens earlier when responses input is unfolded
|
|
389
|
+
// into ChatEnvelope.messages, so the provider request only needs the
|
|
390
|
+
// canonical message list.
|
|
391
|
+
if (payload.max_tokens === undefined && typeof payload.max_output_tokens === 'number') {
|
|
392
|
+
payload.max_tokens = payload.max_output_tokens;
|
|
393
|
+
delete payload.max_output_tokens;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
protocol: 'openai-chat',
|
|
397
|
+
direction: 'response',
|
|
398
|
+
payload,
|
|
399
|
+
meta: {
|
|
400
|
+
context: ctx
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -1,82 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { normalizeChatMessageContentWithNative, normalizeOpenaiChatMessagesWithNative } from '../../../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
|
|
3
|
-
import { ensureProtocolState } from '../../../protocol-state.js';
|
|
4
|
-
import { mapReqInboundBridgeToolsToChatWithNative } from '../../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js';
|
|
5
|
-
const CHAT_PARAMETER_KEYS = [
|
|
6
|
-
'model',
|
|
7
|
-
'temperature',
|
|
8
|
-
'top_p',
|
|
9
|
-
'top_k',
|
|
10
|
-
'max_tokens',
|
|
11
|
-
'frequency_penalty',
|
|
12
|
-
'presence_penalty',
|
|
13
|
-
'logit_bias',
|
|
14
|
-
'response_format',
|
|
15
|
-
'parallel_tool_calls',
|
|
16
|
-
'tool_choice',
|
|
17
|
-
'seed',
|
|
18
|
-
'user',
|
|
19
|
-
'metadata',
|
|
20
|
-
'stop',
|
|
21
|
-
'stop_sequences',
|
|
22
|
-
'stream'
|
|
23
|
-
];
|
|
24
|
-
const KNOWN_TOP_LEVEL_FIELDS = new Set([
|
|
25
|
-
'messages',
|
|
26
|
-
'tools',
|
|
27
|
-
'tool_outputs',
|
|
28
|
-
...CHAT_PARAMETER_KEYS,
|
|
29
|
-
'stageExpectations',
|
|
30
|
-
'stages'
|
|
31
|
-
]);
|
|
32
|
-
function flattenSystemContent(content) {
|
|
33
|
-
if (typeof content === 'string')
|
|
34
|
-
return content;
|
|
35
|
-
if (Array.isArray(content)) {
|
|
36
|
-
return content.map(flattenSystemContent).filter(Boolean).join('\n');
|
|
37
|
-
}
|
|
38
|
-
if (content && typeof content === 'object') {
|
|
39
|
-
const obj = content;
|
|
40
|
-
if (typeof obj.text === 'string')
|
|
41
|
-
return obj.text;
|
|
42
|
-
if (typeof obj.content === 'string')
|
|
43
|
-
return obj.content;
|
|
44
|
-
if (Array.isArray(obj.content))
|
|
45
|
-
return obj.content.map(flattenSystemContent).join('\n');
|
|
46
|
-
}
|
|
47
|
-
return '';
|
|
48
|
-
}
|
|
49
|
-
function normalizeToolContent(content) {
|
|
50
|
-
if (typeof content === 'string')
|
|
51
|
-
return content;
|
|
52
|
-
if (content === null || content === undefined)
|
|
53
|
-
return '';
|
|
54
|
-
try {
|
|
55
|
-
return JSON.stringify(content);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return String(content ?? '');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function maybeAugmentRouteCodexApplyPatchPrecheck(content) {
|
|
62
|
-
if (!content || typeof content !== 'string') {
|
|
63
|
-
return content;
|
|
64
|
-
}
|
|
65
|
-
if (content.includes('[RouteCodex precheck]')) {
|
|
66
|
-
return content;
|
|
67
|
-
}
|
|
68
|
-
const lower = content.toLowerCase();
|
|
69
|
-
if (!lower.includes('failed to parse function arguments')) {
|
|
70
|
-
return content;
|
|
71
|
-
}
|
|
72
|
-
if (content.includes('missing field `input`')) {
|
|
73
|
-
return `${content}\n\n[RouteCodex precheck] apply_patch 参数解析失败:缺少字段 "input"。当前 RouteCodex 期望 { input, patch } 形态,并且两个字段都应包含完整统一 diff 文本。`;
|
|
74
|
-
}
|
|
75
|
-
if (content.includes('invalid type: map, expected a string')) {
|
|
76
|
-
return `${content}\n\n[RouteCodex precheck] apply_patch 参数类型错误:检测到 JSON 对象(map),但客户端期望字符串。请先对参数做 JSON.stringify 再写入 arguments,或直接提供 { patch: "<统一 diff>" } 形式。`;
|
|
77
|
-
}
|
|
78
|
-
return content;
|
|
79
|
-
}
|
|
1
|
+
import { mapOpenaiChatFromChatWithNative, mapOpenaiChatToChatWithNative } from '../../../../router/virtual-router/engine-selection/native-hub-pipeline-semantic-mappers.js';
|
|
80
2
|
export function maybeAugmentApplyPatchErrorContent(content, toolName) {
|
|
81
3
|
if (!content)
|
|
82
4
|
return content;
|
|
@@ -86,319 +8,18 @@ export function maybeAugmentApplyPatchErrorContent(content, toolName) {
|
|
|
86
8
|
if (!isApplyPatch) {
|
|
87
9
|
return content;
|
|
88
10
|
}
|
|
89
|
-
//
|
|
11
|
+
// Avoid duplicate hints.
|
|
90
12
|
if (content.includes('[apply_patch hint]')) {
|
|
91
13
|
return content;
|
|
92
14
|
}
|
|
93
|
-
const hint = '\n\n[apply_patch hint]
|
|
15
|
+
const hint = '\n\n[apply_patch hint] \u5728\u4f7f\u7528 apply_patch \u4e4b\u524d\uff0c\u8bf7\u5148\u8bfb\u53d6\u76ee\u6807\u6587\u4ef6\u7684\u6700\u65b0\u5185\u5bb9\uff0c\u5e76\u57fa\u4e8e\u8be5\u5185\u5bb9\u751f\u6210\u8865\u4e01\uff1b\u540c\u65f6\u786e\u4fdd\u8865\u4e01\u683c\u5f0f\u7b26\u5408\u5de5\u5177\u89c4\u8303\uff08\u7edf\u4e00\u8865\u4e01\u683c\u5f0f\u6216\u7ed3\u6784\u5316\u53c2\u6570\uff09\uff0c\u907f\u514d\u4e0a\u4e0b\u6587\u4e0d\u5339\u914d\u6216\u8bed\u6cd5\u9519\u8bef\u3002';
|
|
94
16
|
return content + hint;
|
|
95
17
|
}
|
|
96
|
-
function recordToolCallIssues(message, messageIndex, missing) {
|
|
97
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : undefined;
|
|
98
|
-
if (!toolCalls?.length)
|
|
99
|
-
return;
|
|
100
|
-
toolCalls.forEach((entry, callIndex) => {
|
|
101
|
-
if (!isJsonObject(entry)) {
|
|
102
|
-
missing.push({
|
|
103
|
-
path: `messages[${messageIndex}].tool_calls[${callIndex}]`,
|
|
104
|
-
reason: 'invalid_tool_call_entry',
|
|
105
|
-
originalValue: jsonClone(entry)
|
|
106
|
-
});
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const fnBlock = entry.function;
|
|
110
|
-
if (!isJsonObject(fnBlock)) {
|
|
111
|
-
missing.push({
|
|
112
|
-
path: `messages[${messageIndex}].tool_calls[${callIndex}].function`,
|
|
113
|
-
reason: 'missing_tool_function',
|
|
114
|
-
originalValue: jsonClone(fnBlock)
|
|
115
|
-
});
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const fnName = fnBlock.name;
|
|
119
|
-
if (typeof fnName !== 'string' || !fnName.trim().length) {
|
|
120
|
-
missing.push({
|
|
121
|
-
path: `messages[${messageIndex}].tool_calls[${callIndex}].function.name`,
|
|
122
|
-
reason: 'missing_tool_name'
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
function collectSystemRawBlocks(raw) {
|
|
128
|
-
if (!Array.isArray(raw))
|
|
129
|
-
return undefined;
|
|
130
|
-
const blocks = [];
|
|
131
|
-
raw.forEach((entry) => {
|
|
132
|
-
if (!isJsonObject(entry))
|
|
133
|
-
return;
|
|
134
|
-
if (String(entry.role ?? '').toLowerCase() !== 'system')
|
|
135
|
-
return;
|
|
136
|
-
blocks.push(jsonClone(entry));
|
|
137
|
-
});
|
|
138
|
-
return blocks.length ? blocks : undefined;
|
|
139
|
-
}
|
|
140
|
-
function normalizeChatMessages(raw) {
|
|
141
|
-
const norm = {
|
|
142
|
-
messages: [],
|
|
143
|
-
systemSegments: [],
|
|
144
|
-
toolOutputs: [],
|
|
145
|
-
missingFields: []
|
|
146
|
-
};
|
|
147
|
-
if (raw === undefined) {
|
|
148
|
-
norm.missingFields.push({ path: 'messages', reason: 'absent' });
|
|
149
|
-
return norm;
|
|
150
|
-
}
|
|
151
|
-
const normalizedRaw = Array.isArray(raw)
|
|
152
|
-
? normalizeOpenaiChatMessagesWithNative(raw)
|
|
153
|
-
: raw;
|
|
154
|
-
if (!Array.isArray(normalizedRaw)) {
|
|
155
|
-
norm.missingFields.push({ path: 'messages', reason: 'invalid_type', originalValue: jsonClone(raw) });
|
|
156
|
-
return norm;
|
|
157
|
-
}
|
|
158
|
-
normalizedRaw.forEach((value, index) => {
|
|
159
|
-
if (!isJsonObject(value)) {
|
|
160
|
-
norm.missingFields.push({ path: `messages[${index}]`, reason: 'invalid_entry', originalValue: jsonClone(value) });
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const roleValue = value.role;
|
|
164
|
-
if (typeof roleValue !== 'string') {
|
|
165
|
-
norm.missingFields.push({ path: `messages[${index}].role`, reason: 'missing_role' });
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const chatMessage = value;
|
|
169
|
-
if (roleValue !== 'system' && roleValue !== 'tool') {
|
|
170
|
-
const normalizedContent = normalizeChatMessageContentWithNative(chatMessage.content);
|
|
171
|
-
const shouldOverwriteContent = !Array.isArray(chatMessage.content);
|
|
172
|
-
if (shouldOverwriteContent && normalizedContent.contentText !== undefined) {
|
|
173
|
-
chatMessage.content = normalizedContent.contentText;
|
|
174
|
-
}
|
|
175
|
-
if (typeof normalizedContent.reasoningText === 'string' && normalizedContent.reasoningText.trim().length) {
|
|
176
|
-
chatMessage.reasoning_content = normalizedContent.reasoningText.trim();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
norm.messages.push(chatMessage);
|
|
180
|
-
const toolCallCandidate = value.tool_calls;
|
|
181
|
-
if (Array.isArray(toolCallCandidate) && toolCallCandidate.length) {
|
|
182
|
-
recordToolCallIssues(value, index, norm.missingFields);
|
|
183
|
-
}
|
|
184
|
-
if (roleValue === 'system') {
|
|
185
|
-
const segment = flattenSystemContent(chatMessage.content);
|
|
186
|
-
if (segment.trim().length) {
|
|
187
|
-
norm.systemSegments.push(segment);
|
|
188
|
-
}
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (roleValue === 'tool') {
|
|
192
|
-
const rawCallId = (value.tool_call_id ?? value.call_id ?? value.id);
|
|
193
|
-
const toolCallId = typeof rawCallId === 'string' && rawCallId.trim().length ? rawCallId.trim() : undefined;
|
|
194
|
-
if (!toolCallId) {
|
|
195
|
-
norm.missingFields.push({ path: `messages[${index}].tool_call_id`, reason: 'missing_tool_call_id' });
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const nameValue = typeof value.name === 'string' && value.name.trim().length ? value.name : undefined;
|
|
199
|
-
const normalizedToolOutput = normalizeToolContent(value.content ?? value.output);
|
|
200
|
-
const routeCodexPrechecked = maybeAugmentRouteCodexApplyPatchPrecheck(normalizedToolOutput);
|
|
201
|
-
if (routeCodexPrechecked !== normalizedToolOutput) {
|
|
202
|
-
// Keep tool role message content aligned with outbound provider requests (e.g. Chat→Responses),
|
|
203
|
-
// while avoiding double-injection.
|
|
204
|
-
if (typeof chatMessage.content === 'string' || chatMessage.content === undefined || chatMessage.content === null) {
|
|
205
|
-
chatMessage.content = routeCodexPrechecked;
|
|
206
|
-
}
|
|
207
|
-
else if (typeof chatMessage.output === 'string') {
|
|
208
|
-
chatMessage.output = routeCodexPrechecked;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const outputEntry = {
|
|
212
|
-
tool_call_id: toolCallId,
|
|
213
|
-
content: routeCodexPrechecked,
|
|
214
|
-
name: nameValue
|
|
215
|
-
};
|
|
216
|
-
outputEntry.content = maybeAugmentApplyPatchErrorContent(outputEntry.content, outputEntry.name);
|
|
217
|
-
norm.toolOutputs.push(outputEntry);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
return norm;
|
|
221
|
-
}
|
|
222
|
-
function normalizeStandaloneToolOutputs(raw, missing) {
|
|
223
|
-
if (!Array.isArray(raw) || raw.length === 0)
|
|
224
|
-
return [];
|
|
225
|
-
const outputs = [];
|
|
226
|
-
raw.forEach((entry, index) => {
|
|
227
|
-
if (!isJsonObject(entry)) {
|
|
228
|
-
missing.push({ path: `tool_outputs[${index}]`, reason: 'invalid_entry', originalValue: jsonClone(entry) });
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const rawCallId = entry.tool_call_id ?? entry.call_id ?? entry.id;
|
|
232
|
-
const toolCallId = typeof rawCallId === 'string' && rawCallId.trim().length ? rawCallId.trim() : undefined;
|
|
233
|
-
if (!toolCallId) {
|
|
234
|
-
missing.push({ path: `tool_outputs[${index}].tool_call_id`, reason: 'missing_tool_call_id' });
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const nameValue = typeof entry.name === 'string' && entry.name.trim().length ? entry.name : undefined;
|
|
238
|
-
const rawContent = normalizeToolContent(entry.content ?? entry.output);
|
|
239
|
-
const content = maybeAugmentApplyPatchErrorContent(rawContent, nameValue);
|
|
240
|
-
outputs.push({
|
|
241
|
-
tool_call_id: toolCallId,
|
|
242
|
-
content,
|
|
243
|
-
name: nameValue
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
return outputs;
|
|
247
|
-
}
|
|
248
|
-
function normalizeTools(raw, missing) {
|
|
249
|
-
if (!Array.isArray(raw) || raw.length === 0) {
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
const tools = mapReqInboundBridgeToolsToChatWithNative(raw);
|
|
253
|
-
if (tools.length === 0) {
|
|
254
|
-
raw.forEach((entry, index) => {
|
|
255
|
-
missing.push({ path: `tools[${index}]`, reason: 'invalid_entry', originalValue: jsonClone(entry) });
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
return tools.length ? tools : undefined;
|
|
259
|
-
}
|
|
260
|
-
function extractParameters(body) {
|
|
261
|
-
const params = {};
|
|
262
|
-
for (const key of CHAT_PARAMETER_KEYS) {
|
|
263
|
-
if (body[key] !== undefined) {
|
|
264
|
-
params[key] = body[key];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return Object.keys(params).length ? params : undefined;
|
|
268
|
-
}
|
|
269
|
-
function collectExtraFields(body) {
|
|
270
|
-
const extras = {};
|
|
271
|
-
for (const [key, value] of Object.entries(body)) {
|
|
272
|
-
if (KNOWN_TOP_LEVEL_FIELDS.has(key)) {
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (value !== undefined) {
|
|
276
|
-
extras[key] = jsonClone(value);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return Object.keys(extras).length ? extras : undefined;
|
|
280
|
-
}
|
|
281
|
-
function extractOpenAIExtraFieldsFromSemantics(semantics) {
|
|
282
|
-
if (!semantics || !semantics.providerExtras || !isJsonObject(semantics.providerExtras)) {
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
const openaiExtras = semantics.providerExtras.openaiChat;
|
|
286
|
-
if (!openaiExtras || !isJsonObject(openaiExtras)) {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
const stored = openaiExtras.extraFields;
|
|
290
|
-
if (!stored || !isJsonObject(stored)) {
|
|
291
|
-
return undefined;
|
|
292
|
-
}
|
|
293
|
-
return stored;
|
|
294
|
-
}
|
|
295
|
-
function hasExplicitEmptyToolsSemantics(semantics) {
|
|
296
|
-
if (!semantics || !semantics.tools || !isJsonObject(semantics.tools)) {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
const flag = semantics.tools.explicitEmpty;
|
|
300
|
-
return flag === true;
|
|
301
|
-
}
|
|
302
|
-
function buildOpenAISemantics(options) {
|
|
303
|
-
const semantics = {};
|
|
304
|
-
if (options.systemSegments && options.systemSegments.length) {
|
|
305
|
-
semantics.system = {
|
|
306
|
-
textBlocks: options.systemSegments.map((segment) => segment)
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
if (options.extraFields && Object.keys(options.extraFields).length) {
|
|
310
|
-
semantics.providerExtras = {
|
|
311
|
-
openaiChat: {
|
|
312
|
-
extraFields: jsonClone(options.extraFields)
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
if (options.explicitEmptyTools) {
|
|
317
|
-
semantics.tools = {
|
|
318
|
-
explicitEmpty: true
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
return Object.keys(semantics).length ? semantics : undefined;
|
|
322
|
-
}
|
|
323
|
-
function applyExtraFields(body, metadata, semantics) {
|
|
324
|
-
const sources = [];
|
|
325
|
-
const semanticsExtras = extractOpenAIExtraFieldsFromSemantics(semantics);
|
|
326
|
-
if (semanticsExtras) {
|
|
327
|
-
sources.push(semanticsExtras);
|
|
328
|
-
}
|
|
329
|
-
if (!sources.length) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
for (const source of sources) {
|
|
333
|
-
for (const [key, value] of Object.entries(source)) {
|
|
334
|
-
if (body[key] !== undefined) {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
body[key] = jsonClone(value);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
18
|
export class ChatSemanticMapper {
|
|
342
19
|
async toChat(format, ctx) {
|
|
343
|
-
|
|
344
|
-
const normalized = normalizeChatMessages(payload.messages);
|
|
345
|
-
const topLevelOutputs = normalizeStandaloneToolOutputs(payload.tool_outputs, normalized.missingFields);
|
|
346
|
-
const toolOutputs = [...normalized.toolOutputs];
|
|
347
|
-
for (const entry of topLevelOutputs) {
|
|
348
|
-
if (!toolOutputs.find(item => item.tool_call_id === entry.tool_call_id)) {
|
|
349
|
-
toolOutputs.push(entry);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
const metadata = { context: ctx };
|
|
353
|
-
const rawSystemBlocks = collectSystemRawBlocks(payload.messages);
|
|
354
|
-
if (rawSystemBlocks) {
|
|
355
|
-
const protocolState = ensureProtocolState(metadata, 'openai');
|
|
356
|
-
protocolState.systemMessages = jsonClone(rawSystemBlocks);
|
|
357
|
-
}
|
|
358
|
-
if (normalized.missingFields.length) {
|
|
359
|
-
metadata.missingFields = normalized.missingFields;
|
|
360
|
-
}
|
|
361
|
-
const extraFields = collectExtraFields(payload);
|
|
362
|
-
const explicitEmptyTools = Array.isArray(payload.tools) && payload.tools.length === 0;
|
|
363
|
-
const semantics = buildOpenAISemantics({
|
|
364
|
-
systemSegments: normalized.systemSegments,
|
|
365
|
-
extraFields,
|
|
366
|
-
explicitEmptyTools
|
|
367
|
-
});
|
|
368
|
-
return {
|
|
369
|
-
messages: normalized.messages,
|
|
370
|
-
tools: normalizeTools(payload.tools, normalized.missingFields),
|
|
371
|
-
toolOutputs: toolOutputs.length ? toolOutputs : undefined,
|
|
372
|
-
parameters: extractParameters(payload),
|
|
373
|
-
semantics,
|
|
374
|
-
metadata
|
|
375
|
-
};
|
|
20
|
+
return mapOpenaiChatToChatWithNative((format.payload ?? {}), ctx);
|
|
376
21
|
}
|
|
377
22
|
async fromChat(chat, ctx) {
|
|
378
|
-
|
|
379
|
-
const payload = {
|
|
380
|
-
messages: chat.messages,
|
|
381
|
-
tools: chat.tools ?? (shouldEmitEmptyTools ? [] : undefined),
|
|
382
|
-
...(chat.parameters || {})
|
|
383
|
-
};
|
|
384
|
-
applyExtraFields(payload, chat.metadata, chat.semantics);
|
|
385
|
-
// Do not forward tool_outputs to provider wire formats. OpenAI Chat
|
|
386
|
-
// endpoints expect tool results to appear as tool role messages, and
|
|
387
|
-
// sending the legacy top-level field causes upstream HTTP 400 responses.
|
|
388
|
-
// Concrete translation happens earlier when responses input is unfolded
|
|
389
|
-
// into ChatEnvelope.messages, so the provider request only needs the
|
|
390
|
-
// canonical message list.
|
|
391
|
-
if (payload.max_tokens === undefined && typeof payload.max_output_tokens === 'number') {
|
|
392
|
-
payload.max_tokens = payload.max_output_tokens;
|
|
393
|
-
delete payload.max_output_tokens;
|
|
394
|
-
}
|
|
395
|
-
return {
|
|
396
|
-
protocol: 'openai-chat',
|
|
397
|
-
direction: 'response',
|
|
398
|
-
payload,
|
|
399
|
-
meta: {
|
|
400
|
-
context: ctx
|
|
401
|
-
}
|
|
402
|
-
};
|
|
23
|
+
return mapOpenaiChatFromChatWithNative(chat, ctx);
|
|
403
24
|
}
|
|
404
25
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createSnapshotWriter } from '../snapshot-utils.js';
|
|
2
|
-
import {
|
|
3
|
-
import { parseLenientJsonishWithNative } from '../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
|
|
2
|
+
import { normalizeSnapshotStagePayloadWithNative } from '../../router/virtual-router/engine-selection/native-snapshot-hooks.js';
|
|
4
3
|
export class SnapshotStageRecorder {
|
|
5
4
|
options;
|
|
6
5
|
writer;
|
|
@@ -22,7 +21,7 @@ export class SnapshotStageRecorder {
|
|
|
22
21
|
if (!this.writer) {
|
|
23
22
|
return;
|
|
24
23
|
}
|
|
25
|
-
const normalized =
|
|
24
|
+
const normalized = normalizeSnapshotStagePayloadWithNative(stage, payload);
|
|
26
25
|
if (!normalized) {
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
@@ -37,91 +36,3 @@ export class SnapshotStageRecorder {
|
|
|
37
36
|
export function createSnapshotRecorder(context, endpoint) {
|
|
38
37
|
return new SnapshotStageRecorder({ context, endpoint });
|
|
39
38
|
}
|
|
40
|
-
const STAGE_KIND_MAP = {
|
|
41
|
-
req_inbound_stage2_semantic_map: 'request_inbound',
|
|
42
|
-
'chat_process.req.stage2.semantic_map': 'request_inbound',
|
|
43
|
-
req_outbound_stage1_semantic_map: 'request_outbound',
|
|
44
|
-
'chat_process.req.stage6.outbound.semantic_map': 'request_outbound',
|
|
45
|
-
resp_inbound_stage3_semantic_map: 'response_inbound',
|
|
46
|
-
'chat_process.resp.stage4.semantic_map_to_chat': 'response_inbound',
|
|
47
|
-
resp_outbound_stage1_client_remap: 'response_outbound',
|
|
48
|
-
'chat_process.resp.stage9.client_remap': 'response_outbound'
|
|
49
|
-
};
|
|
50
|
-
function normalizeStagePayload(stage, payload) {
|
|
51
|
-
const kind = STAGE_KIND_MAP[stage];
|
|
52
|
-
if (!kind) {
|
|
53
|
-
return payload;
|
|
54
|
-
}
|
|
55
|
-
if ((kind === 'request_inbound' || kind === 'request_outbound') && isChatEnvelope(payload)) {
|
|
56
|
-
return buildOpenAIChatSnapshot(payload);
|
|
57
|
-
}
|
|
58
|
-
if (kind === 'response_inbound' || kind === 'response_outbound') {
|
|
59
|
-
return cloneJson(payload);
|
|
60
|
-
}
|
|
61
|
-
return payload;
|
|
62
|
-
}
|
|
63
|
-
function isChatEnvelope(value) {
|
|
64
|
-
return Boolean(value &&
|
|
65
|
-
typeof value === 'object' &&
|
|
66
|
-
Array.isArray(value.messages) &&
|
|
67
|
-
value.metadata &&
|
|
68
|
-
typeof value.metadata === 'object');
|
|
69
|
-
}
|
|
70
|
-
function buildOpenAIChatSnapshot(envelope) {
|
|
71
|
-
const snapshot = {};
|
|
72
|
-
if (envelope.parameters && Object.keys(envelope.parameters).length) {
|
|
73
|
-
Object.assign(snapshot, jsonClone(envelope.parameters));
|
|
74
|
-
}
|
|
75
|
-
snapshot.messages = jsonClone(envelope.messages);
|
|
76
|
-
if (envelope.tools && envelope.tools.length) {
|
|
77
|
-
snapshot.tools = jsonClone(envelope.tools);
|
|
78
|
-
}
|
|
79
|
-
if (envelope.toolOutputs && envelope.toolOutputs.length) {
|
|
80
|
-
snapshot.tool_outputs = jsonClone(envelope.toolOutputs);
|
|
81
|
-
}
|
|
82
|
-
const meta = buildMetaSnapshot(envelope.metadata);
|
|
83
|
-
if (meta) {
|
|
84
|
-
snapshot.meta = meta;
|
|
85
|
-
}
|
|
86
|
-
return snapshot;
|
|
87
|
-
}
|
|
88
|
-
function buildMetaSnapshot(metadata) {
|
|
89
|
-
const meta = {};
|
|
90
|
-
if (metadata.context) {
|
|
91
|
-
meta.context = jsonClone(metadata.context);
|
|
92
|
-
}
|
|
93
|
-
if (Array.isArray(metadata.missingFields) && metadata.missingFields.length) {
|
|
94
|
-
meta.missing_fields = jsonClone(metadata.missingFields);
|
|
95
|
-
}
|
|
96
|
-
const extraKeys = Object.keys(metadata).filter((key) => key !== 'context' && key !== 'missingFields');
|
|
97
|
-
if (extraKeys.length) {
|
|
98
|
-
const extras = {};
|
|
99
|
-
for (const key of extraKeys) {
|
|
100
|
-
const value = metadata[key];
|
|
101
|
-
if (value !== undefined) {
|
|
102
|
-
extras[key] = jsonClone(value);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (Object.keys(extras).length) {
|
|
106
|
-
meta.extra = extras;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return Object.keys(meta).length ? meta : undefined;
|
|
110
|
-
}
|
|
111
|
-
function cloneJson(payload) {
|
|
112
|
-
try {
|
|
113
|
-
const nativeCloned = parseLenientJsonishWithNative(payload);
|
|
114
|
-
if (nativeCloned && typeof nativeCloned === 'object' && !Array.isArray(nativeCloned)) {
|
|
115
|
-
return nativeCloned;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// native clone is best-effort
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
return JSON.parse(JSON.stringify(payload));
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return payload;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
@@ -4,7 +4,7 @@ import type { GovernedChatCompletionPayload, GovernedStandardizedRequest, ToolGo
|
|
|
4
4
|
/**
|
|
5
5
|
* Hybrid governance engine:
|
|
6
6
|
* - request path: native-primary
|
|
7
|
-
* - response path:
|
|
7
|
+
* - response path: native-primary
|
|
8
8
|
*
|
|
9
9
|
* Legacy-only snapshot retained at:
|
|
10
10
|
* - src/conversion/hub/tool-governance/archive/engine.legacy.ts
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DEFAULT_TOOL_GOVERNANCE_RULES } from './rules.js';
|
|
2
|
-
import { governRequestWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.js';
|
|
2
|
+
import { governRequestWithNative, governResponseWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.js';
|
|
3
3
|
/**
|
|
4
4
|
* Hybrid governance engine:
|
|
5
5
|
* - request path: native-primary
|
|
6
|
-
* - response path:
|
|
6
|
+
* - response path: native-primary
|
|
7
7
|
*
|
|
8
8
|
* Legacy-only snapshot retained at:
|
|
9
9
|
* - src/conversion/hub/tool-governance/archive/engine.legacy.ts
|
|
@@ -79,18 +79,24 @@ export class ToolGovernanceEngine {
|
|
|
79
79
|
summary: buildSummary(protocol, 'response', false)
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
let governed;
|
|
83
|
+
try {
|
|
84
|
+
governed = governResponseWithNative({
|
|
85
|
+
payload: payload,
|
|
86
|
+
protocol,
|
|
87
|
+
registry: this.registry
|
|
88
|
+
});
|
|
87
89
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error ?? 'unknown error');
|
|
92
|
+
if (message.includes('Tool name exceeds max length')) {
|
|
93
|
+
throw new ToolGovernanceError(message, protocol, 'response', 'tool.function.name');
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
90
96
|
}
|
|
91
97
|
return {
|
|
92
|
-
payload:
|
|
93
|
-
summary:
|
|
98
|
+
payload: governed.payload,
|
|
99
|
+
summary: governed.summary
|
|
94
100
|
};
|
|
95
101
|
}
|
|
96
102
|
resolveRules(protocol, direction) {
|
|
@@ -99,122 +105,6 @@ export class ToolGovernanceEngine {
|
|
|
99
105
|
return entry?.[direction];
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
|
-
function sanitizeChatCompletionChoice(choice, rules, stats) {
|
|
103
|
-
const message = choice?.message;
|
|
104
|
-
if (!message || typeof message !== 'object') {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const msg = message;
|
|
108
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
109
|
-
msg.tool_calls = msg.tool_calls.map((tc, index) => sanitizeChatCompletionToolCall(tc, rules, stats, `choices[].message.tool_calls[${index}].function.name`));
|
|
110
|
-
}
|
|
111
|
-
if (msg.function_call && typeof msg.function_call === 'object') {
|
|
112
|
-
const orig = msg.function_call.name;
|
|
113
|
-
const sanitizedName = sanitizeName(orig, rules, stats, 'choices[].message.function_call.name');
|
|
114
|
-
msg.function_call.name = sanitizedName;
|
|
115
|
-
}
|
|
116
|
-
if (typeof msg.name === 'string' || msg.role === 'tool') {
|
|
117
|
-
msg.name = sanitizeName(msg.name, rules, stats, 'choices[].message.name');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function sanitizeChatCompletionToolCall(tc, rules, stats, context = 'choices[].message.tool_calls[].function.name') {
|
|
121
|
-
if (!tc || typeof tc !== 'object') {
|
|
122
|
-
return tc;
|
|
123
|
-
}
|
|
124
|
-
const fn = tc.function;
|
|
125
|
-
if (!fn || typeof fn !== 'object') {
|
|
126
|
-
return tc;
|
|
127
|
-
}
|
|
128
|
-
const sanitizedName = sanitizeName(fn.name, rules, stats, context);
|
|
129
|
-
if (sanitizedName === fn.name) {
|
|
130
|
-
return tc;
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
...tc,
|
|
134
|
-
function: {
|
|
135
|
-
...fn,
|
|
136
|
-
name: sanitizedName
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
function createStats(protocol, direction) {
|
|
141
|
-
return {
|
|
142
|
-
protocol,
|
|
143
|
-
direction,
|
|
144
|
-
applied: false,
|
|
145
|
-
sanitizedNames: 0,
|
|
146
|
-
truncatedNames: 0,
|
|
147
|
-
defaultedNames: 0
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function sanitizeName(rawName, rules, stats, field) {
|
|
151
|
-
const defaultName = rules.defaultName ?? 'tool';
|
|
152
|
-
let next = typeof rawName === 'string' ? rawName : '';
|
|
153
|
-
let changed = false;
|
|
154
|
-
if (rules.trimWhitespace !== false) {
|
|
155
|
-
next = next.trim();
|
|
156
|
-
}
|
|
157
|
-
if (!next) {
|
|
158
|
-
next = defaultName;
|
|
159
|
-
stats.defaultedNames += 1;
|
|
160
|
-
changed = true;
|
|
161
|
-
}
|
|
162
|
-
if (rules.forceCase === 'lower') {
|
|
163
|
-
const forced = next.toLowerCase();
|
|
164
|
-
if (forced !== next) {
|
|
165
|
-
next = forced;
|
|
166
|
-
changed = true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
else if (rules.forceCase === 'upper') {
|
|
170
|
-
const forced = next.toUpperCase();
|
|
171
|
-
if (forced !== next) {
|
|
172
|
-
next = forced;
|
|
173
|
-
changed = true;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (rules.allowedCharacters) {
|
|
177
|
-
const matcher = new RegExp(rules.allowedCharacters.source);
|
|
178
|
-
const filtered = next
|
|
179
|
-
.split('')
|
|
180
|
-
.filter((ch) => matcher.test(ch))
|
|
181
|
-
.join('');
|
|
182
|
-
matcher.lastIndex = 0;
|
|
183
|
-
if (filtered.length === 0) {
|
|
184
|
-
next = defaultName;
|
|
185
|
-
stats.defaultedNames += 1;
|
|
186
|
-
changed = true;
|
|
187
|
-
}
|
|
188
|
-
else if (filtered !== next) {
|
|
189
|
-
next = filtered;
|
|
190
|
-
changed = true;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (rules.maxNameLength && next.length > rules.maxNameLength) {
|
|
194
|
-
if (rules.onViolation === 'reject') {
|
|
195
|
-
throw new ToolGovernanceError(`Tool name exceeds max length of ${rules.maxNameLength}`, stats.protocol, stats.direction, field);
|
|
196
|
-
}
|
|
197
|
-
next = next.slice(0, rules.maxNameLength);
|
|
198
|
-
stats.truncatedNames += 1;
|
|
199
|
-
changed = true;
|
|
200
|
-
}
|
|
201
|
-
if (changed || (typeof rawName === 'string' && rawName !== next)) {
|
|
202
|
-
stats.sanitizedNames += 1;
|
|
203
|
-
}
|
|
204
|
-
stats.applied = true;
|
|
205
|
-
return next || defaultName;
|
|
206
|
-
}
|
|
207
|
-
function finalizeSummary(stats) {
|
|
208
|
-
return {
|
|
209
|
-
protocol: stats.protocol,
|
|
210
|
-
direction: stats.direction,
|
|
211
|
-
applied: stats.applied,
|
|
212
|
-
sanitizedNames: stats.sanitizedNames,
|
|
213
|
-
truncatedNames: stats.truncatedNames,
|
|
214
|
-
defaultedNames: stats.defaultedNames,
|
|
215
|
-
timestamp: Date.now()
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
108
|
function buildSummary(protocol, direction, applied) {
|
|
219
109
|
return {
|
|
220
110
|
protocol,
|
|
Binary file
|
package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.d.ts
CHANGED
|
@@ -20,3 +20,11 @@ export declare function governRequestWithNative(input: {
|
|
|
20
20
|
request: Record<string, unknown>;
|
|
21
21
|
summary: Record<string, unknown>;
|
|
22
22
|
};
|
|
23
|
+
export declare function governResponseWithNative(input: {
|
|
24
|
+
payload: Record<string, unknown>;
|
|
25
|
+
protocol?: string;
|
|
26
|
+
registry?: NativeToolGovernanceRegistry;
|
|
27
|
+
}): {
|
|
28
|
+
payload: Record<string, unknown>;
|
|
29
|
+
summary: Record<string, unknown>;
|
|
30
|
+
};
|
package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.js
CHANGED
|
@@ -152,3 +152,51 @@ export function governRequestWithNative(input) {
|
|
|
152
152
|
return fail(reason);
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
export function governResponseWithNative(input) {
|
|
156
|
+
const capability = 'governToolNameResponseJson';
|
|
157
|
+
const fail = (reason) => failNativeRequired(capability, reason);
|
|
158
|
+
if (isNativeDisabledByEnv()) {
|
|
159
|
+
return fail('native disabled');
|
|
160
|
+
}
|
|
161
|
+
const fn = readNativeFunction(capability);
|
|
162
|
+
if (!fn) {
|
|
163
|
+
return fail();
|
|
164
|
+
}
|
|
165
|
+
const normalizedInput = {
|
|
166
|
+
...input,
|
|
167
|
+
registry: normalizeRegistryForNative(input.registry)
|
|
168
|
+
};
|
|
169
|
+
const inputJson = safeStringify(normalizedInput);
|
|
170
|
+
if (!inputJson) {
|
|
171
|
+
return fail('json stringify failed');
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const raw = fn(inputJson);
|
|
175
|
+
const errorReason = readNativeErrorReason(raw);
|
|
176
|
+
if (errorReason) {
|
|
177
|
+
return fail(errorReason);
|
|
178
|
+
}
|
|
179
|
+
if (typeof raw !== 'string' || !raw) {
|
|
180
|
+
return fail('empty result');
|
|
181
|
+
}
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
184
|
+
return fail('invalid payload');
|
|
185
|
+
}
|
|
186
|
+
const row = parsed;
|
|
187
|
+
if (!row.payload || typeof row.payload !== 'object' || Array.isArray(row.payload)) {
|
|
188
|
+
return fail('invalid response payload');
|
|
189
|
+
}
|
|
190
|
+
if (!row.summary || typeof row.summary !== 'object' || Array.isArray(row.summary)) {
|
|
191
|
+
return fail('invalid summary payload');
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
payload: row.payload,
|
|
195
|
+
summary: row.summary
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const reason = error instanceof Error ? error.message : String(error ?? 'unknown');
|
|
200
|
+
return fail(reason);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function mapOpenaiChatToChatWithNative(payload: Record<string, unknown> | null | undefined, adapterContext: Record<string, unknown> | null | undefined): Record<string, unknown>;
|
|
2
|
+
export declare function mapOpenaiChatFromChatWithNative(chatEnvelope: Record<string, unknown> | null | undefined, adapterContext: Record<string, unknown> | null | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { failNativeRequired, isNativeDisabledByEnv } from './native-router-hotpath-policy.js';
|
|
2
|
+
import { loadNativeRouterHotpathBindingForInternalUse } from './native-router-hotpath.js';
|
|
3
|
+
function readNativeFunction(name) {
|
|
4
|
+
const binding = loadNativeRouterHotpathBindingForInternalUse();
|
|
5
|
+
const fn = binding?.[name];
|
|
6
|
+
return typeof fn === 'function' ? fn : null;
|
|
7
|
+
}
|
|
8
|
+
function safeStringify(value) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function parseRecord(raw) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function mapOpenaiChatToChatWithNative(payload, adapterContext) {
|
|
29
|
+
const capability = 'mapOpenaiChatToChatJson';
|
|
30
|
+
const fail = (reason) => failNativeRequired(capability, reason);
|
|
31
|
+
if (isNativeDisabledByEnv()) {
|
|
32
|
+
return fail('native disabled');
|
|
33
|
+
}
|
|
34
|
+
const fn = readNativeFunction(capability);
|
|
35
|
+
if (!fn) {
|
|
36
|
+
return fail();
|
|
37
|
+
}
|
|
38
|
+
const payloadJson = safeStringify(payload ?? {});
|
|
39
|
+
const contextJson = safeStringify(adapterContext ?? {});
|
|
40
|
+
if (!payloadJson || !contextJson) {
|
|
41
|
+
return fail('json stringify failed');
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const raw = fn(payloadJson, contextJson);
|
|
45
|
+
if (typeof raw !== 'string' || !raw) {
|
|
46
|
+
return fail('empty result');
|
|
47
|
+
}
|
|
48
|
+
const parsed = parseRecord(raw);
|
|
49
|
+
return parsed ?? fail('invalid payload');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const reason = error instanceof Error ? error.message : String(error ?? 'unknown');
|
|
53
|
+
return fail(reason);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function mapOpenaiChatFromChatWithNative(chatEnvelope, adapterContext) {
|
|
57
|
+
const capability = 'mapOpenaiChatFromChatJson';
|
|
58
|
+
const fail = (reason) => failNativeRequired(capability, reason);
|
|
59
|
+
if (isNativeDisabledByEnv()) {
|
|
60
|
+
return fail('native disabled');
|
|
61
|
+
}
|
|
62
|
+
const fn = readNativeFunction(capability);
|
|
63
|
+
if (!fn) {
|
|
64
|
+
return fail();
|
|
65
|
+
}
|
|
66
|
+
const chatJson = safeStringify(chatEnvelope ?? {});
|
|
67
|
+
const contextJson = safeStringify(adapterContext ?? {});
|
|
68
|
+
if (!chatJson || !contextJson) {
|
|
69
|
+
return fail('json stringify failed');
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const raw = fn(chatJson, contextJson);
|
|
73
|
+
if (typeof raw !== 'string' || !raw) {
|
|
74
|
+
return fail('empty result');
|
|
75
|
+
}
|
|
76
|
+
const parsed = parseRecord(raw);
|
|
77
|
+
return parsed ?? fail('invalid payload');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const reason = error instanceof Error ? error.message : String(error ?? 'unknown');
|
|
81
|
+
return fail(reason);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -63,6 +63,8 @@ const REQUIRED_NATIVE_EXPORTS = [
|
|
|
63
63
|
'collectToolOutputsJson',
|
|
64
64
|
'buildReqInboundToolOutputSnapshotJson',
|
|
65
65
|
'mapBridgeToolsToChatJson',
|
|
66
|
+
'mapOpenaiChatToChatJson',
|
|
67
|
+
'mapOpenaiChatFromChatJson',
|
|
66
68
|
'captureReqInboundResponsesContextSnapshotJson',
|
|
67
69
|
'computeQuotaBucketsJson',
|
|
68
70
|
'deserializeStopMessageStateJson',
|
|
@@ -103,6 +105,8 @@ const REQUIRED_NATIVE_EXPORTS = [
|
|
|
103
105
|
'enforceChatBudgetJson',
|
|
104
106
|
'resolveBudgetForModelJson',
|
|
105
107
|
'finalizeGovernedRequestJson',
|
|
108
|
+
'governResponseJson',
|
|
109
|
+
'governToolNameResponseJson',
|
|
106
110
|
'findLastUserMessageIndexJson',
|
|
107
111
|
'injectContinueExecutionDirectiveJson',
|
|
108
112
|
'injectTimeTagIntoMessagesJson',
|
|
@@ -137,6 +141,7 @@ const REQUIRED_NATIVE_EXPORTS = [
|
|
|
137
141
|
'normalizeToolSessionMessagesJson',
|
|
138
142
|
'updateToolSessionHistoryJson',
|
|
139
143
|
'normalizeContextCaptureLabelJson',
|
|
144
|
+
'normalizeSnapshotStagePayloadJson',
|
|
140
145
|
'normalizeContextToolsJson',
|
|
141
146
|
'normalizeDueInjectTextJson',
|
|
142
147
|
'normalizeProviderProtocolTokenJson',
|
|
@@ -22,6 +22,14 @@ function parseBoolean(raw) {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
function parseJsonValue(raw) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
25
33
|
export function shouldRecordSnapshotsWithNative() {
|
|
26
34
|
const capability = 'shouldRecordSnapshotsJson';
|
|
27
35
|
const fail = (reason) => failNativeRequired(capability, reason);
|
|
@@ -67,3 +75,35 @@ export function writeSnapshotViaHooksWithNative(options) {
|
|
|
67
75
|
return fail(reason);
|
|
68
76
|
}
|
|
69
77
|
}
|
|
78
|
+
export function normalizeSnapshotStagePayloadWithNative(stage, payload) {
|
|
79
|
+
if (payload === undefined || payload === null) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const capability = 'normalizeSnapshotStagePayloadJson';
|
|
83
|
+
const fail = (reason) => failNativeRequired(capability, reason);
|
|
84
|
+
if (isNativeDisabledByEnv()) {
|
|
85
|
+
return fail('native disabled');
|
|
86
|
+
}
|
|
87
|
+
const fn = readNativeFunction(capability);
|
|
88
|
+
if (!fn) {
|
|
89
|
+
return fail();
|
|
90
|
+
}
|
|
91
|
+
const payloadJson = safeStringify(payload);
|
|
92
|
+
if (!payloadJson) {
|
|
93
|
+
// Preserve non-JSON payloads (e.g. circular structures).
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
const stageToken = typeof stage === 'string' ? stage : '';
|
|
97
|
+
try {
|
|
98
|
+
const raw = fn(stageToken, payloadJson);
|
|
99
|
+
if (typeof raw !== 'string' || !raw) {
|
|
100
|
+
return fail('empty result');
|
|
101
|
+
}
|
|
102
|
+
const parsed = parseJsonValue(raw);
|
|
103
|
+
return parsed === null ? fail('invalid payload') : parsed;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const reason = error instanceof Error ? error.message : String(error ?? 'unknown');
|
|
107
|
+
return fail(reason);
|
|
108
|
+
}
|
|
109
|
+
}
|