@jsonstudio/llms 0.6.631 → 0.6.743
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 +130 -15
- 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/conversion/shared/tooling.d.ts +33 -0
- package/dist/conversion/shared/tooling.js +27 -0
- 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 +410 -67
- package/dist/filters/special/response-tool-arguments-stringify.js +25 -16
- package/dist/filters/special/response-tool-arguments-toon-decode.js +8 -76
- package/dist/filters/utils/snapshot-writer.js +42 -4
- package/dist/guidance/index.js +8 -2
- package/dist/router/virtual-router/engine-health.js +0 -4
- package/dist/router/virtual-router/engine-selection.d.ts +2 -1
- package/dist/router/virtual-router/engine-selection.js +101 -9
- package/dist/router/virtual-router/engine.d.ts +5 -1
- package/dist/router/virtual-router/engine.js +188 -5
- 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 +22 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runChatResponseToolFilters } from '../../../../../shared/tool-filter-pipeline.js';
|
|
2
|
+
import { normalizeApplyPatchToolCallsOnResponse } from '../../../../../shared/tool-governor.js';
|
|
2
3
|
import { ToolGovernanceEngine } from '../../../../tool-governance/index.js';
|
|
3
4
|
import { recordStage } from '../../../stages/utils.js';
|
|
4
5
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
@@ -8,11 +9,12 @@ export async function runRespProcessStage1ToolGovernance(options) {
|
|
|
8
9
|
requestId: options.requestId,
|
|
9
10
|
profile: 'openai-chat'
|
|
10
11
|
});
|
|
11
|
-
const
|
|
12
|
+
const patched = normalizeApplyPatchToolCallsOnResponse(filtered);
|
|
13
|
+
const { payload: governed, summary } = toolGovernanceEngine.governResponse(patched, options.clientProtocol);
|
|
12
14
|
recordStage(options.stageRecorder, 'resp_process_stage1_tool_governance', {
|
|
13
15
|
summary,
|
|
14
16
|
applied: summary?.applied,
|
|
15
|
-
filteredPayload:
|
|
17
|
+
filteredPayload: patched,
|
|
16
18
|
governedPayload: governed
|
|
17
19
|
});
|
|
18
20
|
return { governedPayload: governed };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
3
|
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
4
|
+
import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
|
|
4
5
|
import { isJsonObject } from '../types/json.js';
|
|
5
6
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
6
7
|
export async function runHubChatProcess(options) {
|
|
@@ -79,6 +80,7 @@ async function applyRequestToolGovernance(request, context) {
|
|
|
79
80
|
...merged,
|
|
80
81
|
messages: stripHistoricalImageAttachments(merged.messages)
|
|
81
82
|
};
|
|
83
|
+
merged = normalizeApplyPatchToolCallsOnRequest(merged);
|
|
82
84
|
if (containsImageAttachment(merged.messages)) {
|
|
83
85
|
if (!merged.metadata) {
|
|
84
86
|
merged.metadata = {
|
|
@@ -85,7 +85,12 @@ function resolveChatReasoningMode(_entryEndpoint) {
|
|
|
85
85
|
export async function convertProviderResponse(options) {
|
|
86
86
|
const clientProtocol = resolveClientProtocol(options.entryEndpoint);
|
|
87
87
|
const hasServerToolSupport = Boolean(options.providerInvoker) || Boolean(options.reenterPipeline);
|
|
88
|
-
|
|
88
|
+
// 是否跳过 ServerTool 编排:
|
|
89
|
+
// - 仅在当前 Provider 完全不支持 ServerTool(没有 invoker/reenterPipeline)时跳过;
|
|
90
|
+
// - 对于 serverToolFollowup=true 的二/三跳请求,也允许再次进入 ServerTool 流程,
|
|
91
|
+
// 由具体 handler(例如 gemini_empty_reply_continue / iflow_model_error_retry 等)
|
|
92
|
+
// 自行通过 serverToolFollowup 标记决定是否生效。
|
|
93
|
+
const skipServerTools = !hasServerToolSupport;
|
|
89
94
|
// 对于由 server-side 工具触发的内部跳转(二跳/三跳),统一禁用 SSE 聚合输出,
|
|
90
95
|
// 始终返回完整的 ChatCompletion JSON,便于在 llms 内部直接解析,而不是拿到
|
|
91
96
|
// __sse_responses 可读流。
|
|
@@ -5,9 +5,16 @@ export class SnapshotStageRecorder {
|
|
|
5
5
|
writer;
|
|
6
6
|
constructor(options) {
|
|
7
7
|
this.options = options;
|
|
8
|
+
const contextAny = options.context;
|
|
8
9
|
this.writer = createSnapshotWriter({
|
|
9
10
|
requestId: options.context.requestId,
|
|
10
|
-
endpoint: options.endpoint
|
|
11
|
+
endpoint: options.endpoint,
|
|
12
|
+
providerKey: typeof options.context.providerId === 'string' ? options.context.providerId : undefined,
|
|
13
|
+
groupRequestId: typeof contextAny.clientRequestId === 'string'
|
|
14
|
+
? contextAny.clientRequestId
|
|
15
|
+
: typeof contextAny.groupRequestId === 'string'
|
|
16
|
+
? contextAny.groupRequestId
|
|
17
|
+
: undefined
|
|
11
18
|
});
|
|
12
19
|
}
|
|
13
20
|
record(stage, payload) {
|
|
@@ -43,13 +43,6 @@ export async function canonicalizeOpenAIChatResponse(payload, context, options)
|
|
|
43
43
|
};
|
|
44
44
|
const engine = new FilterEngine();
|
|
45
45
|
engine.registerFilter(new ResponseToolTextCanonicalizeFilter());
|
|
46
|
-
try {
|
|
47
|
-
const { ResponseToolArgumentsToonDecodeFilter } = await import('../../../../../filters/index.js');
|
|
48
|
-
engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter());
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// optional decode filter
|
|
52
|
-
}
|
|
53
46
|
engine.registerFilter(new ResponseToolArgumentsStringifyFilter());
|
|
54
47
|
engine.registerFilter(new ResponseFinishInvariantsFilter());
|
|
55
48
|
const stage1 = await engine.run('response_pre', dto.data, filterContext);
|
|
@@ -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';
|