@jsonstudio/llms 0.6.633 → 0.6.749
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 +0 -5
- package/dist/conversion/codecs/openai-openai-codec.js +0 -6
- package/dist/conversion/codecs/responses-openai-codec.js +1 -7
- package/dist/conversion/hub/node-support.js +5 -4
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
- package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +23 -19
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
- package/dist/conversion/hub/process/chat-process.js +2 -0
- package/dist/conversion/hub/response/provider-response.js +6 -1
- package/dist/conversion/hub/snapshot-recorder.js +8 -1
- package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
- package/dist/conversion/responses/responses-openai-bridge.js +47 -7
- package/dist/conversion/shared/compaction-detect.d.ts +2 -0
- package/dist/conversion/shared/compaction-detect.js +53 -0
- package/dist/conversion/shared/errors.d.ts +1 -1
- package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
- package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
- package/dist/conversion/shared/snapshot-hooks.js +180 -4
- package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
- package/dist/conversion/shared/snapshot-utils.js +4 -0
- package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
- package/dist/conversion/shared/tool-governor.d.ts +2 -0
- package/dist/conversion/shared/tool-governor.js +101 -13
- package/dist/conversion/shared/tool-harvester.js +42 -2
- package/dist/filters/index.d.ts +0 -2
- package/dist/filters/index.js +0 -2
- package/dist/filters/special/request-tools-normalize.d.ts +11 -0
- package/dist/filters/special/request-tools-normalize.js +13 -50
- package/dist/filters/special/response-apply-patch-toon-decode.js +403 -82
- package/dist/filters/special/response-tool-arguments-toon-decode.js +6 -75
- package/dist/filters/utils/snapshot-writer.js +42 -4
- package/dist/guidance/index.js +8 -2
- package/dist/router/virtual-router/bootstrap.js +68 -4
- package/dist/router/virtual-router/engine-health.js +0 -4
- package/dist/router/virtual-router/engine-selection.d.ts +8 -1
- package/dist/router/virtual-router/engine-selection.js +168 -9
- package/dist/router/virtual-router/engine.d.ts +6 -1
- package/dist/router/virtual-router/engine.js +263 -14
- package/dist/router/virtual-router/load-balancer.d.ts +18 -0
- package/dist/router/virtual-router/load-balancer.js +3 -2
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
- package/dist/router/virtual-router/routing-instructions.js +18 -3
- package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
- package/dist/router/virtual-router/sticky-session-store.js +36 -0
- package/dist/router/virtual-router/types.d.ts +29 -0
- package/dist/servertool/engine.js +335 -9
- package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
- package/dist/servertool/handlers/compaction-detect.js +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
- package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
- package/dist/servertool/handlers/stop-message-auto.js +199 -19
- package/dist/servertool/server-side-tools.d.ts +0 -1
- package/dist/servertool/server-side-tools.js +0 -1
- package/dist/servertool/types.d.ts +1 -0
- package/dist/tools/apply-patch-structured.js +52 -15
- package/dist/tools/tool-registry.js +537 -15
- package/dist/utils/toon.d.ts +4 -0
- package/dist/utils/toon.js +75 -0
- package/package.json +4 -2
- package/dist/test-output/virtual-router/results.json +0 -1
- package/dist/test-output/virtual-router/summary.json +0 -12
|
@@ -154,6 +154,46 @@ function normalizeBridgeHistory(seed) {
|
|
|
154
154
|
originalSystemMessages: systemMessages
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
+
function stripRoutingTagsFromText(text) {
|
|
158
|
+
if (typeof text !== 'string') {
|
|
159
|
+
return typeof text === 'string' ? text : String(text ?? '');
|
|
160
|
+
}
|
|
161
|
+
// 移除形如 <** ... **> 的路由指令标记(例如 stopMessage/!provider 等),
|
|
162
|
+
// 避免泄露到上游 provider 或持久化的 Responses 历史中。
|
|
163
|
+
return text.replace(/<\*\*[^*]+\*\*>/g, '').trim();
|
|
164
|
+
}
|
|
165
|
+
function sanitizeBridgeHistory(history) {
|
|
166
|
+
if (!history || typeof history !== 'object') {
|
|
167
|
+
return history;
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(history.input)) {
|
|
170
|
+
for (const entry of history.input) {
|
|
171
|
+
if (!entry || typeof entry !== 'object')
|
|
172
|
+
continue;
|
|
173
|
+
const blocks = entry.content;
|
|
174
|
+
if (!Array.isArray(blocks))
|
|
175
|
+
continue;
|
|
176
|
+
for (const block of blocks) {
|
|
177
|
+
if (!block || typeof block !== 'object')
|
|
178
|
+
continue;
|
|
179
|
+
const record = block;
|
|
180
|
+
if (typeof record.text === 'string') {
|
|
181
|
+
record.text = stripRoutingTagsFromText(record.text);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (typeof history.combinedSystemInstruction === 'string') {
|
|
187
|
+
history.combinedSystemInstruction = stripRoutingTagsFromText(history.combinedSystemInstruction);
|
|
188
|
+
}
|
|
189
|
+
if (typeof history.latestUserInstruction === 'string') {
|
|
190
|
+
history.latestUserInstruction = stripRoutingTagsFromText(history.latestUserInstruction);
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(history.originalSystemMessages)) {
|
|
193
|
+
history.originalSystemMessages = history.originalSystemMessages.map((msg) => stripRoutingTagsFromText(msg));
|
|
194
|
+
}
|
|
195
|
+
return history;
|
|
196
|
+
}
|
|
157
197
|
function mergeResponsesTools(originalTools, fromChat) {
|
|
158
198
|
const result = [];
|
|
159
199
|
const byKey = new Map();
|
|
@@ -296,16 +336,16 @@ export function buildResponsesRequestFromChat(payload, ctx, extras) {
|
|
|
296
336
|
combinedSystemInstruction: ctx.systemInstruction
|
|
297
337
|
}
|
|
298
338
|
: undefined;
|
|
299
|
-
const historySeed = normalizeBridgeHistory(extras?.bridgeHistory) ??
|
|
300
|
-
normalizeBridgeHistory(fallbackHistory) ??
|
|
301
|
-
normalizeBridgeHistory(bridgeMetadata?.bridgeHistory) ??
|
|
302
|
-
normalizeBridgeHistory(envelopeMetadata?.bridgeHistory) ??
|
|
303
|
-
fallbackHistory;
|
|
339
|
+
const historySeed = sanitizeBridgeHistory(normalizeBridgeHistory(extras?.bridgeHistory)) ??
|
|
340
|
+
sanitizeBridgeHistory(normalizeBridgeHistory(fallbackHistory)) ??
|
|
341
|
+
sanitizeBridgeHistory(normalizeBridgeHistory(bridgeMetadata?.bridgeHistory)) ??
|
|
342
|
+
sanitizeBridgeHistory(normalizeBridgeHistory(envelopeMetadata?.bridgeHistory)) ??
|
|
343
|
+
sanitizeBridgeHistory(fallbackHistory);
|
|
304
344
|
const history = historySeed ??
|
|
305
|
-
convertMessagesToBridgeInput({
|
|
345
|
+
sanitizeBridgeHistory(convertMessagesToBridgeInput({
|
|
306
346
|
messages,
|
|
307
347
|
tools: Array.isArray(out.tools) ? out.tools : undefined
|
|
308
|
-
});
|
|
348
|
+
}));
|
|
309
349
|
const callIdTransformer = createToolCallIdTransformer(toolCallIdStyle);
|
|
310
350
|
if (callIdTransformer) {
|
|
311
351
|
enforceToolCallIdStyle(history.input, callIdTransformer);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function hasCompactionMarker(value) {
|
|
2
|
+
const lower = value.toLowerCase();
|
|
3
|
+
return (lower.includes('context checkpoint compaction') ||
|
|
4
|
+
lower.includes('checkpoint compaction') ||
|
|
5
|
+
lower.includes('handoff summary for another llm'));
|
|
6
|
+
}
|
|
7
|
+
function containsMarker(value) {
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
return hasCompactionMarker(value);
|
|
10
|
+
}
|
|
11
|
+
if (!value || typeof value !== 'object') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.some((entry) => containsMarker(entry));
|
|
16
|
+
}
|
|
17
|
+
const record = value;
|
|
18
|
+
if (typeof record.text === 'string' && hasCompactionMarker(record.text)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (typeof record.content === 'string' && hasCompactionMarker(record.content)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(record.content) && record.content.some((entry) => containsMarker(entry))) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(record.parts) && record.parts.some((entry) => containsMarker(entry))) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
export function isCompactionRequest(payload) {
|
|
33
|
+
if (containsMarker(payload)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const messages = payload.messages;
|
|
37
|
+
if (Array.isArray(messages) && messages.some((msg) => containsMarker(msg))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const input = payload.input;
|
|
41
|
+
if (containsMarker(input)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const system = payload.system;
|
|
45
|
+
if (containsMarker(system)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const instructions = payload.instructions;
|
|
49
|
+
if (containsMarker(instructions)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProviderProtocolErrorCode = 'TOOL_PROTOCOL_ERROR' | 'SSE_DECODE_ERROR' | 'MALFORMED_RESPONSE' | 'MALFORMED_REQUEST';
|
|
1
|
+
export type ProviderProtocolErrorCode = 'TOOL_PROTOCOL_ERROR' | 'SSE_DECODE_ERROR' | 'MALFORMED_RESPONSE' | 'MALFORMED_REQUEST' | 'SERVERTOOL_FOLLOWUP_FAILED';
|
|
2
2
|
export type ProviderErrorCategory = 'EXTERNAL_ERROR' | 'TOOL_ERROR' | 'INTERNAL_ERROR';
|
|
3
3
|
export interface ProviderProtocolErrorOptions {
|
|
4
4
|
code: ProviderProtocolErrorCode;
|
|
@@ -96,6 +96,13 @@ export function normalizeMessageReasoningTools(message, options) {
|
|
|
96
96
|
const { cleanedText, toolCalls } = extractToolCallsFromReasoningText(sanitized, { idPrefix });
|
|
97
97
|
const trimmed = (cleanedText || '').trim();
|
|
98
98
|
writeReasoningContent(message, trimmed);
|
|
99
|
+
// Chat 统一行为:如果 message 本身没有正文内容,但存在非空 reasoning_content,
|
|
100
|
+
// 则将思考内容提升到正文,前后包裹 [思考] 标记,避免客户端只看到“空回答”。
|
|
101
|
+
const rawContent = message.content;
|
|
102
|
+
if ((typeof rawContent !== 'string' || rawContent.trim().length === 0) &&
|
|
103
|
+
trimmed.length > 0) {
|
|
104
|
+
message.content = `[思考]\n${trimmed}\n[/思考]`;
|
|
105
|
+
}
|
|
99
106
|
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
100
107
|
return { toolCallsAdded: 0, cleanedReasoning: trimmed };
|
|
101
108
|
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
const DEFAULT_SNAPSHOT_ROOT = path.join(os.homedir(), '.routecodex', 'codex-samples');
|
|
5
|
+
const PENDING_PROVIDER_DIR = '__pending__';
|
|
5
6
|
function resolveSnapshotRoot() {
|
|
6
7
|
const envOverride = process.env.RCC_SNAPSHOT_DIR ||
|
|
7
8
|
process.env.ROUTECODEX_SNAPSHOT_DIR;
|
|
@@ -37,19 +38,194 @@ function channelSuffix(channel) {
|
|
|
37
38
|
const token = sanitizeToken(channel, '');
|
|
38
39
|
return token ? `_${token}` : '';
|
|
39
40
|
}
|
|
41
|
+
function readStringField(value) {
|
|
42
|
+
return typeof value === 'string' && value.trim().length ? value.trim() : undefined;
|
|
43
|
+
}
|
|
44
|
+
function extractNestedProviderKey(value) {
|
|
45
|
+
if (!value || typeof value !== 'object') {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const obj = value;
|
|
49
|
+
const direct = readStringField(obj.providerKey) ||
|
|
50
|
+
readStringField(obj.providerId) ||
|
|
51
|
+
readStringField(obj.profileId);
|
|
52
|
+
if (direct) {
|
|
53
|
+
return direct;
|
|
54
|
+
}
|
|
55
|
+
const target = obj.target;
|
|
56
|
+
if (target && typeof target === 'object') {
|
|
57
|
+
const tk = readStringField(target.providerKey);
|
|
58
|
+
if (tk) {
|
|
59
|
+
return tk;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const meta = obj.meta;
|
|
63
|
+
if (meta && typeof meta === 'object') {
|
|
64
|
+
const m = meta;
|
|
65
|
+
const fromMeta = readStringField(m.providerKey) ||
|
|
66
|
+
readStringField(m.providerId);
|
|
67
|
+
if (fromMeta) {
|
|
68
|
+
return fromMeta;
|
|
69
|
+
}
|
|
70
|
+
const ctx = m.context;
|
|
71
|
+
if (ctx && typeof ctx === 'object') {
|
|
72
|
+
const c = ctx;
|
|
73
|
+
const fromCtx = readStringField(c.providerKey) ||
|
|
74
|
+
readStringField(c.providerId) ||
|
|
75
|
+
readStringField(c.profileId);
|
|
76
|
+
if (fromCtx) {
|
|
77
|
+
return fromCtx;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function extractNestedGroupRequestId(value) {
|
|
84
|
+
if (!value || typeof value !== 'object') {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const obj = value;
|
|
88
|
+
const direct = readStringField(obj.clientRequestId) ||
|
|
89
|
+
readStringField(obj.groupRequestId);
|
|
90
|
+
if (direct) {
|
|
91
|
+
return direct;
|
|
92
|
+
}
|
|
93
|
+
const meta = obj.meta;
|
|
94
|
+
if (meta && typeof meta === 'object') {
|
|
95
|
+
const m = meta;
|
|
96
|
+
const fromMeta = readStringField(m.clientRequestId) ||
|
|
97
|
+
readStringField(m.groupRequestId);
|
|
98
|
+
if (fromMeta) {
|
|
99
|
+
return fromMeta;
|
|
100
|
+
}
|
|
101
|
+
const ctx = m.context;
|
|
102
|
+
if (ctx && typeof ctx === 'object') {
|
|
103
|
+
const c = ctx;
|
|
104
|
+
const fromCtx = readStringField(c.clientRequestId) ||
|
|
105
|
+
readStringField(c.groupRequestId);
|
|
106
|
+
if (fromCtx) {
|
|
107
|
+
return fromCtx;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function toErrorCode(error) {
|
|
114
|
+
if (!error || typeof error !== 'object') {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const code = error.code;
|
|
118
|
+
return typeof code === 'string' && code.trim() ? code : undefined;
|
|
119
|
+
}
|
|
120
|
+
async function writeUniqueFile(dir, baseName, contents) {
|
|
121
|
+
const parsed = path.parse(baseName);
|
|
122
|
+
const ext = parsed.ext || '.json';
|
|
123
|
+
const stem = parsed.name || 'snapshot';
|
|
124
|
+
for (let i = 0; i < 64; i += 1) {
|
|
125
|
+
const name = i === 0 ? `${stem}${ext}` : `${stem}_${i}${ext}`;
|
|
126
|
+
try {
|
|
127
|
+
await fs.writeFile(path.join(dir, name), contents, { encoding: 'utf-8', flag: 'wx' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (toErrorCode(error) === 'EEXIST') {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const fallback = `${stem}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
|
|
138
|
+
await fs.writeFile(path.join(dir, fallback), contents, 'utf-8');
|
|
139
|
+
}
|
|
140
|
+
const requestProviderIndex = globalThis
|
|
141
|
+
.__routecodexSnapshotProviderIndex ||
|
|
142
|
+
new Map();
|
|
143
|
+
globalThis
|
|
144
|
+
.__routecodexSnapshotProviderIndex = requestProviderIndex;
|
|
145
|
+
async function mergeDirs(src, dest) {
|
|
146
|
+
await fs.mkdir(dest, { recursive: true });
|
|
147
|
+
let entries = [];
|
|
148
|
+
try {
|
|
149
|
+
entries = await fs.readdir(src);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const name of entries) {
|
|
155
|
+
const from = path.join(src, name);
|
|
156
|
+
const to = path.join(dest, name);
|
|
157
|
+
try {
|
|
158
|
+
await fs.rename(from, to);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (toErrorCode(error) === 'EEXIST') {
|
|
162
|
+
// avoid overwriting; keep both
|
|
163
|
+
const parsed = path.parse(name);
|
|
164
|
+
const ext = parsed.ext || '';
|
|
165
|
+
const stem = parsed.name || 'snapshot';
|
|
166
|
+
const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
167
|
+
try {
|
|
168
|
+
await fs.rename(from, path.join(dest, `${stem}_${suffix}${ext}`));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// ignore move failure
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// ignore other move failures
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await fs.rmdir(src);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// ignore
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function promotePendingDir(options) {
|
|
186
|
+
if (!options.groupRequestToken || options.providerToken === PENDING_PROVIDER_DIR) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const pending = path.join(options.root, options.folder, PENDING_PROVIDER_DIR, options.groupRequestToken);
|
|
190
|
+
const dest = path.join(options.root, options.folder, options.providerToken, options.groupRequestToken);
|
|
191
|
+
try {
|
|
192
|
+
await fs.access(pending);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
198
|
+
try {
|
|
199
|
+
await fs.rename(pending, dest);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// if rename fails (already exists / cross-device), merge
|
|
203
|
+
await mergeDirs(pending, dest);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
40
206
|
async function writeSnapshotFile(options) {
|
|
41
207
|
const root = resolveSnapshotRoot();
|
|
42
208
|
const folder = resolveSnapshotFolder(options.endpoint);
|
|
43
|
-
const dir = path.join(root, folder);
|
|
44
209
|
const stageToken = sanitizeToken(options.stage, 'snapshot');
|
|
45
|
-
const
|
|
46
|
-
|
|
210
|
+
const groupRequestToken = sanitizeToken(options.groupRequestId ||
|
|
211
|
+
extractNestedGroupRequestId(options.data) ||
|
|
212
|
+
options.requestId, `req_${Date.now()}`);
|
|
213
|
+
const providerFromOptions = readStringField(options.providerKey);
|
|
214
|
+
const providerFromData = extractNestedProviderKey(options.data);
|
|
215
|
+
const knownProvider = requestProviderIndex.get(groupRequestToken);
|
|
216
|
+
const providerToken = sanitizeToken(providerFromOptions || providerFromData || knownProvider || PENDING_PROVIDER_DIR, PENDING_PROVIDER_DIR);
|
|
217
|
+
if (!knownProvider && providerToken !== PENDING_PROVIDER_DIR) {
|
|
218
|
+
requestProviderIndex.set(groupRequestToken, providerToken);
|
|
219
|
+
await promotePendingDir({ root, folder, groupRequestToken, providerToken });
|
|
220
|
+
}
|
|
221
|
+
const dir = path.join(root, folder, providerToken, groupRequestToken);
|
|
47
222
|
await fs.mkdir(dir, { recursive: true });
|
|
48
223
|
const spacing = options.verbosity === 'minimal' ? undefined : 2;
|
|
49
224
|
const payload = spacing !== undefined
|
|
50
225
|
? JSON.stringify(options.data, null, spacing)
|
|
51
226
|
: JSON.stringify(options.data);
|
|
52
|
-
|
|
227
|
+
const fileName = `${stageToken}${channelSuffix(options.channel)}.json`;
|
|
228
|
+
await writeUniqueFile(dir, fileName, payload);
|
|
53
229
|
}
|
|
54
230
|
export async function writeSnapshotViaHooks(options) {
|
|
55
231
|
try {
|
|
@@ -4,6 +4,8 @@ interface SnapshotPayload {
|
|
|
4
4
|
endpoint?: string;
|
|
5
5
|
data: unknown;
|
|
6
6
|
folderHint?: string;
|
|
7
|
+
providerKey?: string;
|
|
8
|
+
groupRequestId?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare function shouldRecordSnapshots(): boolean;
|
|
9
11
|
export declare function recordSnapshot(options: SnapshotPayload): Promise<void>;
|
|
@@ -12,5 +14,7 @@ export declare function createSnapshotWriter(opts: {
|
|
|
12
14
|
requestId: string;
|
|
13
15
|
endpoint?: string;
|
|
14
16
|
folderHint?: string;
|
|
17
|
+
providerKey?: string;
|
|
18
|
+
groupRequestId?: string;
|
|
15
19
|
}): SnapshotWriter | undefined;
|
|
16
20
|
export {};
|
|
@@ -31,6 +31,8 @@ export async function recordSnapshot(options) {
|
|
|
31
31
|
endpoint,
|
|
32
32
|
stage: options.stage,
|
|
33
33
|
requestId: options.requestId,
|
|
34
|
+
providerKey: options.providerKey,
|
|
35
|
+
groupRequestId: options.groupRequestId,
|
|
34
36
|
data: options.data,
|
|
35
37
|
verbosity: 'verbose'
|
|
36
38
|
}).catch(() => {
|
|
@@ -48,6 +50,8 @@ export function createSnapshotWriter(opts) {
|
|
|
48
50
|
requestId: opts.requestId,
|
|
49
51
|
endpoint,
|
|
50
52
|
folderHint: opts.folderHint,
|
|
53
|
+
providerKey: opts.providerKey,
|
|
54
|
+
groupRequestId: opts.groupRequestId,
|
|
51
55
|
data: payload
|
|
52
56
|
});
|
|
53
57
|
};
|
|
@@ -234,19 +234,13 @@ export async function runChatResponseToolFilters(chatJson, options = {}) {
|
|
|
234
234
|
registeredStages.add(filter.stage);
|
|
235
235
|
engine.registerFilter(filter);
|
|
236
236
|
};
|
|
237
|
-
const { ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } = await import('../../filters/index.js');
|
|
237
|
+
const { ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
|
|
238
238
|
register(new ResponseToolTextCanonicalizeFilter());
|
|
239
239
|
try {
|
|
240
|
-
|
|
241
|
-
register(new ResponseToolArgumentsToonDecodeFilter());
|
|
242
|
-
register(new ResponseApplyPatchToonDecodeFilter());
|
|
243
|
-
try {
|
|
244
|
-
register(new ResponseToolArgumentsSchemaConvergeFilter());
|
|
245
|
-
}
|
|
246
|
-
catch { /* optional */ }
|
|
247
|
-
register(new ResponseToolArgumentsBlacklistFilter());
|
|
240
|
+
register(new ResponseToolArgumentsSchemaConvergeFilter());
|
|
248
241
|
}
|
|
249
242
|
catch { /* optional */ }
|
|
243
|
+
register(new ResponseToolArgumentsBlacklistFilter());
|
|
250
244
|
register(new ResponseToolArgumentsStringifyFilter());
|
|
251
245
|
register(new ResponseFinishInvariantsFilter());
|
|
252
246
|
try {
|
|
@@ -9,6 +9,8 @@ export interface ToolGovernanceOptions {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
export declare function processChatRequestTools(request: Unknown, opts?: ToolGovernanceOptions): Unknown;
|
|
12
|
+
export declare function normalizeApplyPatchToolCallsOnResponse(chat: Unknown): Unknown;
|
|
13
|
+
export declare function normalizeApplyPatchToolCallsOnRequest(request: Unknown): Unknown;
|
|
12
14
|
export declare function processChatResponseTools(resp: Unknown): Unknown;
|
|
13
15
|
export interface GovernContext extends ToolGovernanceOptions {
|
|
14
16
|
phase: 'request' | 'response';
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// enforceChatBudget: 为避免在请求侧引入多余依赖,这里提供最小实现(保留形状,不裁剪)。
|
|
5
5
|
import { augmentOpenAITools } from '../../guidance/index.js';
|
|
6
6
|
import { validateToolCall } from '../../tools/tool-registry.js';
|
|
7
|
+
import { repairFindMeta } from './tooling.js';
|
|
7
8
|
function isObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
8
9
|
// Note: tool schema strict augmentation removed per alignment
|
|
9
10
|
function enforceChatBudget(chat, _modelId) { return chat; }
|
|
@@ -25,8 +26,8 @@ function tryWriteSnapshot(options, stage, data) {
|
|
|
25
26
|
const ep = String(snap.endpoint || 'chat').toLowerCase();
|
|
26
27
|
const group = ep.includes('responses') ? 'openai-responses' : ep.includes('messages') ? 'anthropic-messages' : 'openai-chat';
|
|
27
28
|
const rid = String(snap.requestId || `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`);
|
|
28
|
-
const dir = path.join(base, group);
|
|
29
|
-
const file = path.join(dir,
|
|
29
|
+
const dir = path.join(base, group, '__pending__', rid);
|
|
30
|
+
const file = path.join(dir, `govern-${stage}.json`);
|
|
30
31
|
if (fs.existsSync(file))
|
|
31
32
|
return; // 不重复
|
|
32
33
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -209,7 +210,7 @@ export function processChatRequestTools(request, opts) {
|
|
|
209
210
|
// 4) Enforce payload budget (context bytes) with minimal loss policy
|
|
210
211
|
const modelId = typeof canonical?.model === 'string' ? String(canonical.model) : 'unknown';
|
|
211
212
|
const budgeted = enforceChatBudget(canonical, modelId);
|
|
212
|
-
return
|
|
213
|
+
return normalizeSpecialToolCallsOnRequest(budgeted);
|
|
213
214
|
}
|
|
214
215
|
catch {
|
|
215
216
|
return out;
|
|
@@ -220,7 +221,7 @@ export function processChatRequestTools(request, opts) {
|
|
|
220
221
|
* - Canonicalize textual tool markup to tool_calls; ensure finish_reason and content=null policy
|
|
221
222
|
*/
|
|
222
223
|
import { repairArgumentsToString, parseLenient } from './jsonish.js';
|
|
223
|
-
function normalizeApplyPatchToolCallsOnResponse(chat) {
|
|
224
|
+
export function normalizeApplyPatchToolCallsOnResponse(chat) {
|
|
224
225
|
try {
|
|
225
226
|
const out = JSON.parse(JSON.stringify(chat));
|
|
226
227
|
const choices = Array.isArray(out?.choices) ? out.choices : [];
|
|
@@ -241,6 +242,19 @@ function normalizeApplyPatchToolCallsOnResponse(chat) {
|
|
|
241
242
|
if (validation && validation.ok && typeof validation.normalizedArgs === 'string') {
|
|
242
243
|
fn.arguments = validation.normalizedArgs;
|
|
243
244
|
}
|
|
245
|
+
else if (validation && !validation.ok) {
|
|
246
|
+
try {
|
|
247
|
+
const reason = validation.reason ?? 'unknown';
|
|
248
|
+
const snippet = typeof argsStr === 'string' && argsStr.trim().length
|
|
249
|
+
? argsStr.trim().slice(0, 200).replace(/\s+/g, ' ')
|
|
250
|
+
: '';
|
|
251
|
+
// eslint-disable-next-line no-console
|
|
252
|
+
console.error(`\x1b[31m[apply_patch][precheck][response] validation_failed reason=${reason}${snippet ? ` args=${snippet}` : ''}\x1b[0m`);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// logging best-effort
|
|
256
|
+
}
|
|
257
|
+
}
|
|
244
258
|
}
|
|
245
259
|
catch {
|
|
246
260
|
// best-effort per tool_call
|
|
@@ -253,14 +267,32 @@ function normalizeApplyPatchToolCallsOnResponse(chat) {
|
|
|
253
267
|
return chat;
|
|
254
268
|
}
|
|
255
269
|
}
|
|
256
|
-
function normalizeApplyPatchToolCallsOnRequest(request) {
|
|
270
|
+
export function normalizeApplyPatchToolCallsOnRequest(request) {
|
|
271
|
+
return normalizeSpecialToolCallsOnRequest(request);
|
|
272
|
+
}
|
|
273
|
+
function normalizeSpecialToolCallsOnRequest(request) {
|
|
257
274
|
try {
|
|
258
275
|
const out = JSON.parse(JSON.stringify(request));
|
|
259
276
|
const messages = Array.isArray(out?.messages) ? out.messages : [];
|
|
260
|
-
|
|
277
|
+
// 仅针对「当轮」工具调用做校验与形态修复:选择最后一条 assistant 消息
|
|
278
|
+
let lastAssistantIndex = -1;
|
|
279
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
280
|
+
const candidate = messages[i];
|
|
281
|
+
if (!candidate || typeof candidate !== 'object')
|
|
282
|
+
continue;
|
|
283
|
+
if (candidate.role === 'assistant') {
|
|
284
|
+
lastAssistantIndex = i;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (lastAssistantIndex === -1) {
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
292
|
+
const msg = messages[i];
|
|
261
293
|
if (!msg || typeof msg !== 'object')
|
|
262
294
|
continue;
|
|
263
|
-
if (
|
|
295
|
+
if (i !== lastAssistantIndex)
|
|
264
296
|
continue;
|
|
265
297
|
const tcs = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
266
298
|
if (!tcs.length)
|
|
@@ -269,13 +301,48 @@ function normalizeApplyPatchToolCallsOnRequest(request) {
|
|
|
269
301
|
try {
|
|
270
302
|
const fn = tc && tc.function ? tc.function : undefined;
|
|
271
303
|
const name = typeof fn?.name === 'string' ? String(fn.name).trim().toLowerCase() : '';
|
|
272
|
-
if (name !== 'apply_patch')
|
|
273
|
-
continue;
|
|
274
304
|
const rawArgs = fn?.arguments;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
305
|
+
// apply_patch 兼容:统一生成 { patch, input }
|
|
306
|
+
if (name === 'apply_patch') {
|
|
307
|
+
const argsStr = repairArgumentsToString(rawArgs);
|
|
308
|
+
const validation = validateToolCall('apply_patch', argsStr);
|
|
309
|
+
if (validation && validation.ok && typeof validation.normalizedArgs === 'string') {
|
|
310
|
+
fn.arguments = validation.normalizedArgs;
|
|
311
|
+
}
|
|
312
|
+
else if (validation && !validation.ok) {
|
|
313
|
+
try {
|
|
314
|
+
const reason = validation.reason ?? 'unknown';
|
|
315
|
+
const snippet = typeof argsStr === 'string' && argsStr.trim().length
|
|
316
|
+
? argsStr.trim().slice(0, 200).replace(/\s+/g, ' ')
|
|
317
|
+
: '';
|
|
318
|
+
// eslint-disable-next-line no-console
|
|
319
|
+
console.error(`\x1b[31m[apply_patch][precheck][request] validation_failed reason=${reason}${snippet ? ` args=${snippet}` : ''}\x1b[0m`);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// logging best-effort
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// exec_command 兼容:TOON / map / string 一律收敛为 { cmd, command, workdir, ... }
|
|
328
|
+
if (name === 'exec_command') {
|
|
329
|
+
const argsStr = repairArgumentsToString(rawArgs);
|
|
330
|
+
let parsed;
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(argsStr);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
parsed = parseLenient(argsStr);
|
|
336
|
+
}
|
|
337
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
338
|
+
const normalized = normalizeExecCommandArgs(parsed);
|
|
339
|
+
try {
|
|
340
|
+
fn.arguments = JSON.stringify(normalized ?? {});
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
fn.arguments = '{}';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
279
346
|
}
|
|
280
347
|
}
|
|
281
348
|
catch {
|
|
@@ -289,6 +356,27 @@ function normalizeApplyPatchToolCallsOnRequest(request) {
|
|
|
289
356
|
return request;
|
|
290
357
|
}
|
|
291
358
|
}
|
|
359
|
+
function normalizeExecCommandArgs(args) {
|
|
360
|
+
try {
|
|
361
|
+
const out = { ...args };
|
|
362
|
+
const rawCmd = typeof out.cmd === 'string' && out.cmd.trim().length
|
|
363
|
+
? String(out.cmd)
|
|
364
|
+
: typeof out.command === 'string' && out.command.trim().length
|
|
365
|
+
? String(out.command)
|
|
366
|
+
: undefined;
|
|
367
|
+
if (rawCmd) {
|
|
368
|
+
const fixed = repairFindMeta(rawCmd);
|
|
369
|
+
out.cmd = fixed;
|
|
370
|
+
if (typeof out.command === 'string') {
|
|
371
|
+
out.command = fixed;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return args;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
292
380
|
function enhanceResponseToolArguments(chat) {
|
|
293
381
|
try {
|
|
294
382
|
const enable = String(process?.env?.RCC_TOOL_ENHANCE ?? '1').trim() !== '0';
|
|
@@ -80,8 +80,48 @@ function extractFromTextual(content, ctx) {
|
|
|
80
80
|
}
|
|
81
81
|
return events;
|
|
82
82
|
}
|
|
83
|
-
// 3)
|
|
84
|
-
|
|
83
|
+
// 3) <invoke name="tool"> wrapper with <parameter name="key">value</parameter>
|
|
84
|
+
try {
|
|
85
|
+
const invokeRe = /<invoke\s+name="([^">]+)"[^>]*>([\s\S]*?)<\/invoke>/gi;
|
|
86
|
+
let mInvoke;
|
|
87
|
+
const invokeEvents = [];
|
|
88
|
+
let idx = 0;
|
|
89
|
+
while ((mInvoke = invokeRe.exec(content)) !== null) {
|
|
90
|
+
const toolName = (mInvoke[1] || '').trim();
|
|
91
|
+
const inner = mInvoke[2] || '';
|
|
92
|
+
if (!toolName || !inner)
|
|
93
|
+
continue;
|
|
94
|
+
const paramRe = /<parameter\s+name="([^">]+)"[^>]*>([\s\S]*?)<\/parameter>/gi;
|
|
95
|
+
const argsObj = {};
|
|
96
|
+
let mParam;
|
|
97
|
+
while ((mParam = paramRe.exec(inner)) !== null) {
|
|
98
|
+
const key = String((mParam[1] || '').trim());
|
|
99
|
+
const raw = (mParam[2] || '').trim();
|
|
100
|
+
if (!key)
|
|
101
|
+
continue;
|
|
102
|
+
let value = raw;
|
|
103
|
+
try {
|
|
104
|
+
value = JSON.parse(raw);
|
|
105
|
+
}
|
|
106
|
+
catch { /* keep string */ }
|
|
107
|
+
argsObj[key] = value;
|
|
108
|
+
}
|
|
109
|
+
const id = genId(ctx, idx);
|
|
110
|
+
invokeEvents.push({ tool_calls: [{ index: idx, id, type: 'function', function: { name: toolName } }] });
|
|
111
|
+
const argStr = toJsonString(argsObj);
|
|
112
|
+
const parts = chunkString(argStr, Math.max(32, Math.min(1024, ctx?.chunkSize || 256)));
|
|
113
|
+
for (const d of parts) {
|
|
114
|
+
invokeEvents.push({ tool_calls: [{ index: idx, id, type: 'function', function: { arguments: d } }] });
|
|
115
|
+
}
|
|
116
|
+
idx += 1;
|
|
117
|
+
}
|
|
118
|
+
if (invokeEvents.length) {
|
|
119
|
+
return invokeEvents;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { /* ignore textual invoke parse errors */ }
|
|
123
|
+
// 4) rcc.tool.v1 JSON envelope (removed)
|
|
124
|
+
// 5) Generic <tool_call> textual block with <arg_key>/<arg_value>
|
|
85
125
|
try {
|
|
86
126
|
// Explicit wrapper form
|
|
87
127
|
const blockRe = /<tool_call[^>]*>[\s\S]*?<\/tool_call>/gi;
|
package/dist/filters/index.d.ts
CHANGED
|
@@ -12,8 +12,6 @@ export * from './special/response-tool-text-canonicalize.js';
|
|
|
12
12
|
export * from './special/response-tool-arguments-stringify.js';
|
|
13
13
|
export * from './special/response-finish-invariants.js';
|
|
14
14
|
export * from './special/request-tools-normalize.js';
|
|
15
|
-
export * from './special/response-tool-arguments-toon-decode.js';
|
|
16
|
-
export * from './special/response-apply-patch-toon-decode.js';
|
|
17
15
|
export * from './special/response-tool-arguments-blacklist.js';
|
|
18
16
|
export * from './special/response-tool-arguments-schema-converge.js';
|
|
19
17
|
export * from './special/response-tool-arguments-whitelist.js';
|