@jsonstudio/llms 0.6.3271 → 0.6.3379
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/bridge-message-utils.d.ts +4 -4
- package/dist/conversion/bridge-message-utils.js +28 -538
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +38 -0
- package/dist/conversion/compat/profiles/responses-crs.json +15 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +16 -5
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +1 -6
- package/dist/conversion/hub/response/response-runtime.js +14 -6
- package/dist/conversion/responses/responses-openai-bridge/response-payload.js +11 -11
- package/dist/conversion/shared/anthropic-message-utils.js +2 -12
- package/dist/conversion/shared/chat-request-filters.js +2 -61
- package/dist/conversion/shared/reasoning-mapping.js +3 -0
- package/dist/conversion/shared/reasoning-normalizer.d.ts +1 -0
- package/dist/conversion/shared/reasoning-normalizer.js +35 -388
- package/dist/conversion/shared/reasoning-tool-normalizer.js +8 -15
- package/dist/conversion/shared/reasoning-utils.js +13 -35
- package/dist/conversion/shared/responses-tool-utils.d.ts +1 -1
- package/dist/conversion/shared/responses-tool-utils.js +63 -65
- package/dist/conversion/shared/streaming-text-extractor.d.ts +0 -5
- package/dist/conversion/shared/streaming-text-extractor.js +18 -111
- package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +1 -1
- package/dist/conversion/shared/text-markup-normalizer/normalize.js +3 -91
- package/dist/conversion/shared/thought-signature-validator.js +19 -133
- package/dist/conversion/shared/tool-argument-repairer.js +16 -19
- package/dist/conversion/shared/tool-call-id-manager.d.ts +1 -5
- package/dist/conversion/shared/tool-call-id-manager.js +74 -98
- package/dist/conversion/shared/tool-harvester.js +19 -382
- package/dist/conversion/shared/tool-mapping.d.ts +2 -3
- package/dist/conversion/shared/tool-mapping.js +28 -184
- package/dist/conversion/shared/tooling.js +20 -151
- package/dist/filters/special/response-tool-arguments-stringify.js +9 -1
- package/dist/guidance/index.js +2 -2
- package/dist/native/router_hotpath_napi.node +0 -0
- package/dist/router/virtual-router/engine-legacy/helpers.js +1 -1
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.d.ts +39 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.js +196 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js +27 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +34 -0
- package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.d.ts +65 -1
- package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.js +836 -35
- package/dist/router/virtual-router/engine.js +3 -2
- package/dist/router/virtual-router/routing-instructions/parse.js +30 -3
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +28 -3
- package/dist/sse/types/anthropic-types.d.ts +3 -1
- package/dist/tools/apply-patch/args-normalizer/extract-patch.js +2 -2
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +3 -6
- package/dist/tools/apply-patch/patch-text/normalize.js +14 -3
- package/dist/tools/tool-registry.js +12 -0
- package/package.json +6 -1
|
@@ -1,75 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createToolCallIdTransformerWithNative, normalizeFunctionCallIdWithNative, normalizeFunctionCallOutputIdWithNative, normalizeResponsesCallIdWithNative } from '../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
|
|
2
2
|
import { sanitizeResponsesFunctionNameWithNative } from '../../router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.js';
|
|
3
|
+
function assertResponsesToolUtilsNativeAvailable() {
|
|
4
|
+
if (typeof createToolCallIdTransformerWithNative !== 'function' ||
|
|
5
|
+
typeof normalizeFunctionCallIdWithNative !== 'function' ||
|
|
6
|
+
typeof normalizeFunctionCallOutputIdWithNative !== 'function' ||
|
|
7
|
+
typeof normalizeResponsesCallIdWithNative !== 'function' ||
|
|
8
|
+
typeof sanitizeResponsesFunctionNameWithNative !== 'function') {
|
|
9
|
+
throw new Error('[responses-tool-utils] native bindings unavailable');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
export function createToolCallIdTransformer(style) {
|
|
13
|
+
assertResponsesToolUtilsNativeAvailable();
|
|
4
14
|
if (style !== 'fc') {
|
|
5
15
|
return null;
|
|
6
16
|
}
|
|
7
|
-
|
|
8
|
-
let itemCounter = 0;
|
|
9
|
-
let outputCounter = 0;
|
|
10
|
-
const callAlias = new Map();
|
|
11
|
-
const normalizeCallId = (raw) => {
|
|
12
|
-
const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
|
|
13
|
-
if (rawStr && callAlias.has(rawStr)) {
|
|
14
|
-
return callAlias.get(rawStr);
|
|
15
|
-
}
|
|
16
|
-
const normalized = normalizeResponsesCallId({
|
|
17
|
-
callId: rawStr,
|
|
18
|
-
fallback: `call_${++callCounter}`
|
|
19
|
-
});
|
|
20
|
-
if (rawStr) {
|
|
21
|
-
callAlias.set(rawStr, normalized);
|
|
22
|
-
}
|
|
23
|
-
return normalized;
|
|
24
|
-
};
|
|
25
|
-
const normalizeItemId = (raw, callId) => {
|
|
26
|
-
const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
|
|
27
|
-
return normalizeFunctionCallId({
|
|
28
|
-
callId: rawStr ?? callId,
|
|
29
|
-
fallback: `fc_item_${++itemCounter}`
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
const normalizeOutputId = (callId, raw) => {
|
|
33
|
-
const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
|
|
34
|
-
return normalizeFunctionCallOutputId({
|
|
35
|
-
callId,
|
|
36
|
-
fallback: rawStr ?? `fc_tool_${++outputCounter}`
|
|
37
|
-
});
|
|
38
|
-
};
|
|
17
|
+
const state = createToolCallIdTransformerWithNative(style);
|
|
39
18
|
return {
|
|
40
|
-
normalizeCallId
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
normalizeCallId(raw) {
|
|
20
|
+
return normalizeResponsesCallIdWithNative({
|
|
21
|
+
callId: typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined,
|
|
22
|
+
fallback: transformCounter(state, 'call')
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
normalizeItemId(raw, callId) {
|
|
26
|
+
return normalizeFunctionCallIdWithNative({
|
|
27
|
+
callId: typeof raw === 'string' && raw.trim().length ? raw.trim() : callId,
|
|
28
|
+
fallback: transformCounter(state, 'item')
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
normalizeOutputId(callId, raw) {
|
|
32
|
+
return normalizeFunctionCallOutputIdWithNative({
|
|
33
|
+
callId,
|
|
34
|
+
fallback: typeof raw === 'string' && raw.trim().length ? raw.trim() : transformCounter(state, 'tool')
|
|
35
|
+
});
|
|
36
|
+
}
|
|
43
37
|
};
|
|
44
38
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (type === 'function_call') {
|
|
51
|
-
const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.id);
|
|
52
|
-
entry.call_id = normalizedCallId;
|
|
53
|
-
entry.id = transformer.normalizeItemId(entry.id ?? normalizedCallId, normalizedCallId);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (type === 'function_call_output' || type === 'tool_result' || type === 'tool_message') {
|
|
57
|
-
const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.tool_call_id ?? entry.id);
|
|
58
|
-
entry.call_id = normalizedCallId;
|
|
59
|
-
// OpenAI `/v1/responses` request schema uses `call_id` for tool outputs.
|
|
60
|
-
// Some internal carriers may include `tool_call_id`; strip it before sending upstream
|
|
61
|
-
// to avoid strict schema errors (e.g. "Unknown parameter: input[N].tool_call_id").
|
|
62
|
-
if (entry.tool_call_id !== undefined) {
|
|
63
|
-
delete entry.tool_call_id;
|
|
64
|
-
}
|
|
65
|
-
entry.id = transformer.normalizeOutputId(normalizedCallId, entry.id);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
39
|
+
function transformCounter(state, prefix) {
|
|
40
|
+
const current = typeof state.__counter === 'number' ? state.__counter : 0;
|
|
41
|
+
const next = current + 1;
|
|
42
|
+
state.__counter = next;
|
|
43
|
+
return `${prefix}_${next}`;
|
|
68
44
|
}
|
|
69
45
|
function isStableToolCallId(raw) {
|
|
70
46
|
return /^((fc|call)_[A-Za-z0-9_-]+)$/i.test(raw);
|
|
71
47
|
}
|
|
72
48
|
export function normalizeResponsesToolCallIds(payload) {
|
|
49
|
+
assertResponsesToolUtilsNativeAvailable();
|
|
73
50
|
if (!payload || typeof payload !== 'object') {
|
|
74
51
|
return;
|
|
75
52
|
}
|
|
@@ -85,7 +62,7 @@ export function normalizeResponsesToolCallIds(payload) {
|
|
|
85
62
|
}
|
|
86
63
|
const normalized = trimmed && isStableToolCallId(trimmed)
|
|
87
64
|
? trimmed
|
|
88
|
-
:
|
|
65
|
+
: normalizeFunctionCallIdWithNative({
|
|
89
66
|
callId: trimmed || undefined,
|
|
90
67
|
fallback: nextFallback(fallbackPrefix)
|
|
91
68
|
});
|
|
@@ -106,7 +83,7 @@ export function normalizeResponsesToolCallIds(payload) {
|
|
|
106
83
|
item.tool_call_id = normalizedCallId;
|
|
107
84
|
}
|
|
108
85
|
const rawOutputId = typeof item.id === 'string' ? item.id : undefined;
|
|
109
|
-
item.id =
|
|
86
|
+
item.id = normalizeFunctionCallOutputIdWithNative({
|
|
110
87
|
callId: normalizedCallId,
|
|
111
88
|
fallback: rawOutputId ?? nextFallback('fc')
|
|
112
89
|
});
|
|
@@ -119,7 +96,7 @@ export function normalizeResponsesToolCallIds(payload) {
|
|
|
119
96
|
item.tool_call_id = normalizedCallId;
|
|
120
97
|
}
|
|
121
98
|
const rawOutputId = typeof item.id === 'string' ? item.id : undefined;
|
|
122
|
-
item.id =
|
|
99
|
+
item.id = normalizeFunctionCallOutputIdWithNative({
|
|
123
100
|
callId: normalizedCallId,
|
|
124
101
|
fallback: rawOutputId ?? nextFallback('fc')
|
|
125
102
|
});
|
|
@@ -155,8 +132,6 @@ export function normalizeResponsesToolCallIds(payload) {
|
|
|
155
132
|
}
|
|
156
133
|
}
|
|
157
134
|
export function resolveToolCallIdStyle(metadata) {
|
|
158
|
-
// Standard OpenAI `/v1/responses` requires function_call item ids to start with `fc_`.
|
|
159
|
-
// Default to `fc` unless explicitly overridden (e.g. LM Studio compat).
|
|
160
135
|
if (!metadata)
|
|
161
136
|
return 'fc';
|
|
162
137
|
const raw = metadata.toolCallIdStyle;
|
|
@@ -171,6 +146,7 @@ export function resolveToolCallIdStyle(metadata) {
|
|
|
171
146
|
}
|
|
172
147
|
return 'fc';
|
|
173
148
|
}
|
|
149
|
+
const RAW_SYSTEM_SENTINEL = '__rcc_raw_system';
|
|
174
150
|
export function stripInternalToolingMetadata(metadata) {
|
|
175
151
|
if (!metadata || typeof metadata !== 'object')
|
|
176
152
|
return;
|
|
@@ -203,7 +179,29 @@ function prunePrivateExtraFields(target) {
|
|
|
203
179
|
}
|
|
204
180
|
}
|
|
205
181
|
}
|
|
206
|
-
const RAW_SYSTEM_SENTINEL = '__rcc_raw_system';
|
|
207
182
|
export function sanitizeResponsesFunctionName(rawName) {
|
|
183
|
+
assertResponsesToolUtilsNativeAvailable();
|
|
208
184
|
return sanitizeResponsesFunctionNameWithNative(rawName);
|
|
209
185
|
}
|
|
186
|
+
export function enforceToolCallIdStyle(input, transformer) {
|
|
187
|
+
assertResponsesToolUtilsNativeAvailable();
|
|
188
|
+
for (const entry of input) {
|
|
189
|
+
if (!entry || typeof entry !== 'object')
|
|
190
|
+
continue;
|
|
191
|
+
const type = typeof entry.type === 'string' ? entry.type.toLowerCase() : '';
|
|
192
|
+
if (type === 'function_call') {
|
|
193
|
+
const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.id);
|
|
194
|
+
entry.call_id = normalizedCallId;
|
|
195
|
+
entry.id = transformer.normalizeItemId(entry.id ?? normalizedCallId, normalizedCallId);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (type === 'function_call_output' || type === 'tool_result' || type === 'tool_message') {
|
|
199
|
+
const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.tool_call_id ?? entry.id);
|
|
200
|
+
entry.call_id = normalizedCallId;
|
|
201
|
+
if (entry.tool_call_id !== undefined) {
|
|
202
|
+
delete entry.tool_call_id;
|
|
203
|
+
}
|
|
204
|
+
entry.id = transformer.normalizeOutputId(normalizedCallId, entry.id);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -16,10 +16,5 @@ export declare class StreamingTextToolExtractor {
|
|
|
16
16
|
constructor(opts?: StreamingToolExtractorOptions);
|
|
17
17
|
reset(): void;
|
|
18
18
|
feedText(text: string): StreamingToolCall[];
|
|
19
|
-
private genId;
|
|
20
|
-
private toToolCall;
|
|
21
|
-
private tryExtractStructuredBlocks;
|
|
22
|
-
private tryExtractFunctionExecuteBlocks;
|
|
23
|
-
private splitCommand;
|
|
24
19
|
}
|
|
25
20
|
export declare function createStreamingToolExtractor(opts?: StreamingToolExtractorOptions): StreamingTextToolExtractor;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Streaming textual tool intent extractor (对齐)
|
|
2
2
|
// Detects <function=execute> blocks and structured apply_patch payloads
|
|
3
3
|
// and converts them into OpenAI tool_calls incrementally.
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { extractStreamingToolCallsWithNative } from '../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
|
|
5
|
+
function assertStreamingToolExtractorNativeAvailable() {
|
|
6
|
+
if (typeof extractStreamingToolCallsWithNative !== 'function') {
|
|
7
|
+
throw new Error('[streaming-text-extractor] native bindings unavailable');
|
|
8
|
+
}
|
|
8
9
|
}
|
|
9
10
|
export class StreamingTextToolExtractor {
|
|
10
11
|
opts;
|
|
@@ -18,114 +19,20 @@ export class StreamingTextToolExtractor {
|
|
|
18
19
|
this.idCounter = 0;
|
|
19
20
|
}
|
|
20
21
|
feedText(text) {
|
|
21
|
-
const out = [];
|
|
22
22
|
if (typeof text !== 'string' || !text)
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const argStr = repairArgumentsToStringWithNative(argsObj);
|
|
37
|
-
return { id: this.genId(), type: 'function', function: { name, arguments: argStr } };
|
|
38
|
-
}
|
|
39
|
-
tryExtractStructuredBlocks() {
|
|
40
|
-
const out = [];
|
|
41
|
-
let searchIdx = 0;
|
|
42
|
-
while (searchIdx < this.buffer.length) {
|
|
43
|
-
const startIdx = this.buffer.indexOf('```', searchIdx);
|
|
44
|
-
if (startIdx < 0)
|
|
45
|
-
break;
|
|
46
|
-
const headerEnd = this.buffer.indexOf('\n', startIdx + 3);
|
|
47
|
-
if (headerEnd < 0)
|
|
48
|
-
break;
|
|
49
|
-
const language = this.buffer.slice(startIdx + 3, headerEnd).trim().toLowerCase();
|
|
50
|
-
const endIdx = this.buffer.indexOf('```', headerEnd + 1);
|
|
51
|
-
if (endIdx < 0)
|
|
52
|
-
break;
|
|
53
|
-
const body = this.buffer.slice(headerEnd + 1, endIdx);
|
|
54
|
-
if (!language || language === 'json' || language === 'apply_patch' || language === 'toon') {
|
|
55
|
-
try {
|
|
56
|
-
const parsed = JSON.parse(body);
|
|
57
|
-
if (isStructuredApplyPatchPayload(parsed)) {
|
|
58
|
-
out.push(this.toToolCall('apply_patch', parsed));
|
|
59
|
-
this.buffer = this.buffer.slice(0, startIdx) + this.buffer.slice(endIdx + 3);
|
|
60
|
-
searchIdx = 0;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
/* ignore parse errors */
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
searchIdx = endIdx + 3;
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
// rcc.tool.v1 JSON detection removed
|
|
73
|
-
tryExtractFunctionExecuteBlocks() {
|
|
74
|
-
const out = [];
|
|
75
|
-
// Very lightweight parser for <function=execute> <parameter=command>...</parameter>
|
|
76
|
-
const execRe = /<function=execute>[\s\S]*?<parameter=command>([\s\S]*?)<\/parameter>[\s\S]*?<\/function=execute>/gi;
|
|
77
|
-
let m;
|
|
78
|
-
while ((m = execRe.exec(this.buffer)) !== null) {
|
|
79
|
-
const cmd = (m[1] || '').trim();
|
|
80
|
-
if (cmd) {
|
|
81
|
-
// split a shell command string into argv best-effort
|
|
82
|
-
const argv = this.splitCommand(cmd);
|
|
83
|
-
out.push(this.toToolCall('shell', { command: argv }));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (out.length > 0) {
|
|
87
|
-
this.buffer = this.buffer.replace(execRe, '');
|
|
88
|
-
}
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
splitCommand(s) {
|
|
92
|
-
try {
|
|
93
|
-
// basic quotes-aware split
|
|
94
|
-
const out = [];
|
|
95
|
-
let cur = '';
|
|
96
|
-
let quote = null;
|
|
97
|
-
for (let i = 0; i < s.length; i++) {
|
|
98
|
-
const ch = s[i];
|
|
99
|
-
if (quote) {
|
|
100
|
-
if (ch === quote) {
|
|
101
|
-
quote = null;
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
cur += ch;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
if (ch === '"' || ch === '\'') {
|
|
109
|
-
quote = ch;
|
|
110
|
-
}
|
|
111
|
-
else if (/\s/.test(ch)) {
|
|
112
|
-
if (cur) {
|
|
113
|
-
out.push(cur);
|
|
114
|
-
cur = '';
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
cur += ch;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (cur)
|
|
123
|
-
out.push(cur);
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return [s];
|
|
128
|
-
}
|
|
23
|
+
return [];
|
|
24
|
+
assertStreamingToolExtractorNativeAvailable();
|
|
25
|
+
const idPrefix = this.opts.idPrefix || 'call';
|
|
26
|
+
const output = extractStreamingToolCallsWithNative({
|
|
27
|
+
buffer: this.buffer,
|
|
28
|
+
text,
|
|
29
|
+
idPrefix,
|
|
30
|
+
idCounter: this.idCounter,
|
|
31
|
+
nowMs: Date.now()
|
|
32
|
+
});
|
|
33
|
+
this.buffer = typeof output.buffer === 'string' ? output.buffer : this.buffer;
|
|
34
|
+
this.idCounter = typeof output.idCounter === 'number' ? output.idCounter : this.idCounter;
|
|
35
|
+
return Array.isArray(output.toolCalls) ? output.toolCalls : [];
|
|
129
36
|
}
|
|
130
37
|
}
|
|
131
38
|
export function createStreamingToolExtractor(opts) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { TextMarkupNormalizeOptions } from '../../types/text-markup-normalizer.js';
|
|
2
|
-
export declare function normalizeAssistantTextToToolCalls(message: Record<string, any>,
|
|
2
|
+
export declare function normalizeAssistantTextToToolCalls(message: Record<string, any>, options?: TextMarkupNormalizeOptions): Record<string, any>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeAssistantTextToToolCallsWithNative } from '../../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
|
|
2
2
|
function enabled() {
|
|
3
3
|
try {
|
|
4
4
|
return String(process?.env?.RCC_TEXT_MARKUP_COMPAT ?? '1').trim() !== '0';
|
|
@@ -7,96 +7,8 @@ function enabled() {
|
|
|
7
7
|
return true;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
export function normalizeAssistantTextToToolCalls(message,
|
|
10
|
+
export function normalizeAssistantTextToToolCalls(message, options) {
|
|
11
11
|
if (!enabled())
|
|
12
12
|
return message;
|
|
13
|
-
|
|
14
|
-
if (!message || typeof message !== 'object')
|
|
15
|
-
return message;
|
|
16
|
-
if (Array.isArray(message.tool_calls) && message.tool_calls.length)
|
|
17
|
-
return message;
|
|
18
|
-
const content = message.content;
|
|
19
|
-
const candidates = [];
|
|
20
|
-
if (typeof content === 'string') {
|
|
21
|
-
candidates.push(content);
|
|
22
|
-
}
|
|
23
|
-
else if (Array.isArray(content)) {
|
|
24
|
-
for (const part of content) {
|
|
25
|
-
if (!part || typeof part !== 'object')
|
|
26
|
-
continue;
|
|
27
|
-
const p = part;
|
|
28
|
-
if (typeof p.text === 'string' && p.text.trim()) {
|
|
29
|
-
candidates.push(p.text);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (typeof p.content === 'string' && p.content.trim()) {
|
|
33
|
-
candidates.push(p.content);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (typeof message.reasoning === 'string' && message.reasoning.trim()) {
|
|
39
|
-
candidates.push(String(message.reasoning));
|
|
40
|
-
}
|
|
41
|
-
if (typeof message.thinking === 'string' && message.thinking.trim()) {
|
|
42
|
-
candidates.push(String(message.thinking));
|
|
43
|
-
}
|
|
44
|
-
if (!candidates.length)
|
|
45
|
-
return message;
|
|
46
|
-
let calls = null;
|
|
47
|
-
const mapNativeToolCalls = (raw) => {
|
|
48
|
-
const mapped = [];
|
|
49
|
-
raw.forEach((entry, index) => {
|
|
50
|
-
if (!entry || typeof entry !== 'object')
|
|
51
|
-
return;
|
|
52
|
-
const functionNode = entry.function && typeof entry.function === 'object' && !Array.isArray(entry.function)
|
|
53
|
-
? entry.function
|
|
54
|
-
: undefined;
|
|
55
|
-
const name = (typeof functionNode?.name === 'string' && functionNode.name.trim()) ||
|
|
56
|
-
(typeof entry.name === 'string' && entry.name.trim()) ||
|
|
57
|
-
undefined;
|
|
58
|
-
if (!name)
|
|
59
|
-
return;
|
|
60
|
-
const argsCandidate = functionNode?.arguments ?? entry.arguments ?? entry.args ?? '{}';
|
|
61
|
-
let args = '{}';
|
|
62
|
-
if (typeof argsCandidate === 'string') {
|
|
63
|
-
args = argsCandidate;
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
try {
|
|
67
|
-
args = JSON.stringify(argsCandidate ?? {});
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
args = '{}';
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const id = (typeof entry.id === 'string' && entry.id.trim()) ||
|
|
74
|
-
(typeof entry.call_id === 'string' && entry.call_id.trim()) ||
|
|
75
|
-
`call_${index + 1}`;
|
|
76
|
-
mapped.push({ id, name, args });
|
|
77
|
-
});
|
|
78
|
-
return mapped;
|
|
79
|
-
};
|
|
80
|
-
for (const text of candidates) {
|
|
81
|
-
const nativeParsed = extractToolCallsFromReasoningTextWithNative(text);
|
|
82
|
-
if (nativeParsed.toolCalls.length) {
|
|
83
|
-
const nativeCalls = mapNativeToolCalls(nativeParsed.toolCalls);
|
|
84
|
-
if (nativeCalls.length) {
|
|
85
|
-
calls = nativeCalls;
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (calls && calls.length) {
|
|
91
|
-
const toolCalls = calls.map((c) => ({ id: c.id, type: 'function', function: { name: c.name, arguments: c.args } }));
|
|
92
|
-
const copy = { ...message };
|
|
93
|
-
copy.tool_calls = toolCalls;
|
|
94
|
-
copy.content = '';
|
|
95
|
-
return copy;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// ignore normalization failures
|
|
100
|
-
}
|
|
101
|
-
return message;
|
|
13
|
+
return normalizeAssistantTextToToolCallsWithNative(message, options);
|
|
102
14
|
}
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
* 提供严格的 thoughtSignature 验证功能,用于 Claude/Gemini thinking 块的签名验证。
|
|
5
5
|
* 参考 gcli2api 的实现,确保与上游 API 的兼容性。
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
import { filterInvalidThinkingBlocksWithNative, hasValidThoughtSignatureWithNative, removeTrailingUnsignedThinkingBlocksWithNative, sanitizeThinkingBlockWithNative } from '../../router/virtual-router/engine-selection/native-shared-conversion-semantics.js';
|
|
8
|
+
function assertThoughtSignatureValidatorNativeAvailable() {
|
|
9
|
+
if (typeof hasValidThoughtSignatureWithNative !== 'function' ||
|
|
10
|
+
typeof sanitizeThinkingBlockWithNative !== 'function' ||
|
|
11
|
+
typeof filterInvalidThinkingBlocksWithNative !== 'function' ||
|
|
12
|
+
typeof removeTrailingUnsignedThinkingBlocksWithNative !== 'function') {
|
|
13
|
+
throw new Error('[thought-signature-validator] native bindings unavailable');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
14
16
|
/**
|
|
15
17
|
* 检查 thinking 块是否有有效的 thoughtSignature
|
|
16
18
|
*
|
|
@@ -19,26 +21,8 @@ const DEFAULT_OPTIONS = {
|
|
|
19
21
|
* @returns 是否有效
|
|
20
22
|
*/
|
|
21
23
|
export function hasValidThoughtSignature(block, options) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
const obj = block;
|
|
26
|
-
const blockType = obj.type;
|
|
27
|
-
if (blockType !== 'thinking' && blockType !== 'reasoning' && blockType !== 'redacted_thinking') {
|
|
28
|
-
return true; // 非 thinking 块默认有效
|
|
29
|
-
}
|
|
30
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
31
|
-
const thinking = sanitizeReasoningTaggedTextWithNative(String(obj.thinking || obj.text || ''));
|
|
32
|
-
const signature = coerceThoughtSignature(obj.thoughtSignature || obj.signature);
|
|
33
|
-
// 空 thinking + 任意签名 = 有效 (trailing signature case)
|
|
34
|
-
if (!thinking.trim() && signature !== undefined && opts.allowEmptyThinkingWithSignature) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
// 有内容 + 足够长度的 signature = 有效
|
|
38
|
-
if (signature && signature.length >= opts.minLength) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
24
|
+
assertThoughtSignatureValidatorNativeAvailable();
|
|
25
|
+
return hasValidThoughtSignatureWithNative(block, options);
|
|
42
26
|
}
|
|
43
27
|
/**
|
|
44
28
|
* 清理 thinking 块,移除额外字段,保留有效签名
|
|
@@ -47,23 +31,8 @@ export function hasValidThoughtSignature(block, options) {
|
|
|
47
31
|
* @returns 清理后的块
|
|
48
32
|
*/
|
|
49
33
|
export function sanitizeThinkingBlock(block) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
const obj = block;
|
|
54
|
-
const blockType = obj.type;
|
|
55
|
-
if (blockType !== 'thinking' && blockType !== 'reasoning' && blockType !== 'redacted_thinking') {
|
|
56
|
-
return obj;
|
|
57
|
-
}
|
|
58
|
-
const sanitized = {
|
|
59
|
-
type: blockType,
|
|
60
|
-
thinking: sanitizeReasoningTaggedTextWithNative(String(obj.thinking || obj.text || ''))
|
|
61
|
-
};
|
|
62
|
-
const signature = coerceThoughtSignature(obj.thoughtSignature || obj.signature);
|
|
63
|
-
if (signature) {
|
|
64
|
-
sanitized.thoughtSignature = signature;
|
|
65
|
-
}
|
|
66
|
-
return sanitized;
|
|
34
|
+
assertThoughtSignatureValidatorNativeAvailable();
|
|
35
|
+
return sanitizeThinkingBlockWithNative(block);
|
|
67
36
|
}
|
|
68
37
|
/**
|
|
69
38
|
* 过滤消息中的无效 thinking 块
|
|
@@ -72,58 +41,9 @@ export function sanitizeThinkingBlock(block) {
|
|
|
72
41
|
* @param options - 验证选项
|
|
73
42
|
*/
|
|
74
43
|
export function filterInvalidThinkingBlocks(messages, options) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!msg || typeof msg !== 'object')
|
|
79
|
-
continue;
|
|
80
|
-
const msgObj = msg;
|
|
81
|
-
const role = msgObj.role;
|
|
82
|
-
// 只处理 assistant 和 model 消息
|
|
83
|
-
if (role !== 'assistant' && role !== 'model')
|
|
84
|
-
continue;
|
|
85
|
-
const content = msgObj.content;
|
|
86
|
-
if (!Array.isArray(content))
|
|
87
|
-
continue;
|
|
88
|
-
const originalLen = content.length;
|
|
89
|
-
const newBlocks = [];
|
|
90
|
-
for (const block of content) {
|
|
91
|
-
if (!block || typeof block !== 'object') {
|
|
92
|
-
newBlocks.push(block);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const blockType = block.type;
|
|
96
|
-
if (blockType !== 'thinking' && blockType !== 'reasoning' && blockType !== 'redacted_thinking') {
|
|
97
|
-
newBlocks.push(block);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
// 检查 thinking 块的有效性
|
|
101
|
-
if (hasValidThoughtSignature(block, opts)) {
|
|
102
|
-
// 有效签名,清理后保留
|
|
103
|
-
newBlocks.push(sanitizeThinkingBlock(block));
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
// 无效签名
|
|
107
|
-
const thinkingText = String(block.thinking || block.text || '');
|
|
108
|
-
if (thinkingText.trim() && opts.convertToTextOnFailure) {
|
|
109
|
-
// 有内容,转换为文本
|
|
110
|
-
newBlocks.push({ type: 'text', text: thinkingText });
|
|
111
|
-
}
|
|
112
|
-
// 否则丢弃(空 thinking + 无效签名)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
msgObj.content = newBlocks;
|
|
116
|
-
const filteredCount = originalLen - newBlocks.length;
|
|
117
|
-
totalFiltered += filteredCount;
|
|
118
|
-
// 如果过滤后为空,添加一个空文本块以保持消息有效
|
|
119
|
-
if (!newBlocks.length) {
|
|
120
|
-
msgObj.content = [{ type: 'text', text: '' }];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (totalFiltered > 0) {
|
|
124
|
-
// 可以在这里添加日志记录
|
|
125
|
-
console.debug(`[ThoughtSignatureValidator] Filtered ${totalFiltered} invalid thinking block(s)`);
|
|
126
|
-
}
|
|
44
|
+
assertThoughtSignatureValidatorNativeAvailable();
|
|
45
|
+
const normalized = filterInvalidThinkingBlocksWithNative(messages, options);
|
|
46
|
+
messages.splice(0, messages.length, ...normalized);
|
|
127
47
|
}
|
|
128
48
|
/**
|
|
129
49
|
* 移除末尾未签名的 thinking 块
|
|
@@ -132,41 +52,7 @@ export function filterInvalidThinkingBlocks(messages, options) {
|
|
|
132
52
|
* @param options - 验证选项
|
|
133
53
|
*/
|
|
134
54
|
export function removeTrailingUnsignedThinkingBlocks(blocks, options) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let end_index = blocks.length;
|
|
139
|
-
// 从末尾向前查找第一个有效签名的 thinking 块
|
|
140
|
-
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
141
|
-
const block = blocks[i];
|
|
142
|
-
if (!block || typeof block !== 'object')
|
|
143
|
-
continue;
|
|
144
|
-
const blockType = block.type;
|
|
145
|
-
if (blockType === 'thinking' || blockType === 'reasoning' || blockType === 'redacted_thinking') {
|
|
146
|
-
if (!hasValidThoughtSignature(block, opts)) {
|
|
147
|
-
end_index = i;
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
break; // 遇到有效签名的 thinking 块,停止
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
break; // 遇到非 thinking 块,停止
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// 移除末尾未签名的 thinking 块
|
|
158
|
-
if (end_index < blocks.length) {
|
|
159
|
-
const removed = blocks.length - end_index;
|
|
160
|
-
blocks.splice(end_index);
|
|
161
|
-
console.debug(`[ThoughtSignatureValidator] Removed ${removed} trailing unsigned thinking block(s)`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* 提取并强制转换 thoughtSignature
|
|
166
|
-
*/
|
|
167
|
-
function coerceThoughtSignature(value) {
|
|
168
|
-
if (typeof value === 'string' && value.trim().length) {
|
|
169
|
-
return value.trim();
|
|
170
|
-
}
|
|
171
|
-
return undefined;
|
|
55
|
+
assertThoughtSignatureValidatorNativeAvailable();
|
|
56
|
+
const normalized = removeTrailingUnsignedThinkingBlocksWithNative(blocks, options);
|
|
57
|
+
blocks.splice(0, blocks.length, ...normalized);
|
|
172
58
|
}
|