@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.
Files changed (64) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.js +0 -5
  2. package/dist/conversion/codecs/openai-openai-codec.js +0 -6
  3. package/dist/conversion/codecs/responses-openai-codec.js +1 -7
  4. package/dist/conversion/hub/node-support.js +5 -4
  5. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
  6. package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
  7. package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +130 -15
  9. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
  10. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
  11. package/dist/conversion/hub/process/chat-process.js +2 -0
  12. package/dist/conversion/hub/response/provider-response.js +6 -1
  13. package/dist/conversion/hub/snapshot-recorder.js +8 -1
  14. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
  15. package/dist/conversion/responses/responses-openai-bridge.js +47 -7
  16. package/dist/conversion/shared/compaction-detect.d.ts +2 -0
  17. package/dist/conversion/shared/compaction-detect.js +53 -0
  18. package/dist/conversion/shared/errors.d.ts +1 -1
  19. package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
  20. package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
  21. package/dist/conversion/shared/snapshot-hooks.js +180 -4
  22. package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
  23. package/dist/conversion/shared/snapshot-utils.js +4 -0
  24. package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
  25. package/dist/conversion/shared/tool-governor.d.ts +2 -0
  26. package/dist/conversion/shared/tool-governor.js +101 -13
  27. package/dist/conversion/shared/tool-harvester.js +42 -2
  28. package/dist/conversion/shared/tooling.d.ts +33 -0
  29. package/dist/conversion/shared/tooling.js +27 -0
  30. package/dist/filters/index.d.ts +0 -2
  31. package/dist/filters/index.js +0 -2
  32. package/dist/filters/special/request-tools-normalize.d.ts +11 -0
  33. package/dist/filters/special/request-tools-normalize.js +13 -50
  34. package/dist/filters/special/response-apply-patch-toon-decode.js +410 -67
  35. package/dist/filters/special/response-tool-arguments-stringify.js +25 -16
  36. package/dist/filters/special/response-tool-arguments-toon-decode.js +8 -76
  37. package/dist/filters/utils/snapshot-writer.js +42 -4
  38. package/dist/guidance/index.js +8 -2
  39. package/dist/router/virtual-router/engine-health.js +0 -4
  40. package/dist/router/virtual-router/engine-selection.d.ts +2 -1
  41. package/dist/router/virtual-router/engine-selection.js +101 -9
  42. package/dist/router/virtual-router/engine.d.ts +5 -1
  43. package/dist/router/virtual-router/engine.js +188 -5
  44. package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
  45. package/dist/router/virtual-router/routing-instructions.js +18 -3
  46. package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
  47. package/dist/router/virtual-router/sticky-session-store.js +36 -0
  48. package/dist/router/virtual-router/types.d.ts +22 -0
  49. package/dist/servertool/engine.js +335 -9
  50. package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
  51. package/dist/servertool/handlers/compaction-detect.js +1 -0
  52. package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
  53. package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
  54. package/dist/servertool/handlers/stop-message-auto.js +199 -19
  55. package/dist/servertool/server-side-tools.d.ts +0 -1
  56. package/dist/servertool/server-side-tools.js +0 -1
  57. package/dist/servertool/types.d.ts +1 -0
  58. package/dist/tools/apply-patch-structured.js +52 -15
  59. package/dist/tools/tool-registry.js +537 -15
  60. package/dist/utils/toon.d.ts +4 -0
  61. package/dist/utils/toon.js +75 -0
  62. package/package.json +4 -2
  63. package/dist/test-output/virtual-router/results.json +0 -1
  64. 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 { payload: governed, summary } = toolGovernanceEngine.governResponse(filtered, options.clientProtocol);
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: filtered,
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
- const skipServerTools = isServerToolFollowup(options.context) || !hasServerToolSupport;
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,2 @@
1
+ import type { JsonObject } from '../hub/types/json.js';
2
+ export declare function isCompactionRequest(payload: JsonObject): boolean;
@@ -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
  }
@@ -5,5 +5,7 @@ export interface SnapshotHookOptions {
5
5
  data: unknown;
6
6
  verbosity?: 'minimal' | 'verbose';
7
7
  channel?: string;
8
+ providerKey?: string;
9
+ groupRequestId?: string;
8
10
  }
9
11
  export declare function writeSnapshotViaHooks(options: SnapshotHookOptions): Promise<void>;
@@ -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 requestToken = sanitizeToken(options.requestId, `req_${Date.now()}`);
46
- const filePath = path.join(dir, `${requestToken}_${stageToken}${channelSuffix(options.channel)}.json`);
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
- await fs.writeFile(filePath, payload, 'utf-8');
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
- const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
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';