@jsonstudio/rcc 0.89.935 → 0.89.1083

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 (96) hide show
  1. package/README.md +1 -42
  2. package/dist/build-info.js +2 -2
  3. package/dist/build-info.js.map +1 -1
  4. package/dist/cli.js +120 -16
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/quota-daemon.d.ts +2 -0
  7. package/dist/commands/quota-daemon.js +89 -0
  8. package/dist/commands/quota-daemon.js.map +1 -0
  9. package/dist/commands/token-daemon.js +1 -1
  10. package/dist/commands/token-daemon.js.map +1 -1
  11. package/dist/docs/daemon-admin-ui.html +958 -0
  12. package/dist/index.js +54 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/manager/modules/quota/index.d.ts +34 -0
  15. package/dist/manager/modules/quota/index.js +291 -0
  16. package/dist/manager/modules/quota/index.js.map +1 -1
  17. package/dist/manager/modules/token/index.js +14 -3
  18. package/dist/manager/modules/token/index.js.map +1 -1
  19. package/dist/manager/quota/provider-quota-center.d.ts +48 -0
  20. package/dist/manager/quota/provider-quota-center.js +239 -0
  21. package/dist/manager/quota/provider-quota-center.js.map +1 -0
  22. package/dist/manager/quota/provider-quota-store.d.ts +17 -0
  23. package/dist/manager/quota/provider-quota-store.js +88 -0
  24. package/dist/manager/quota/provider-quota-store.js.map +1 -0
  25. package/dist/providers/auth/token-scanner/index.js +11 -3
  26. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  27. package/dist/providers/core/runtime/http-request-executor.js +24 -7
  28. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  29. package/dist/providers/core/runtime/http-transport-provider.js +11 -3
  30. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  31. package/dist/providers/core/runtime/responses-provider.js +9 -3
  32. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  33. package/dist/providers/core/utils/http-client.d.ts +1 -0
  34. package/dist/providers/core/utils/http-client.js +139 -4
  35. package/dist/providers/core/utils/http-client.js.map +1 -1
  36. package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
  37. package/dist/providers/core/utils/snapshot-writer.js +99 -18
  38. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  39. package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
  40. package/dist/providers/mock/mock-provider-runtime.js +176 -4
  41. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  42. package/dist/server/handlers/chat-handler.js +13 -1
  43. package/dist/server/handlers/chat-handler.js.map +1 -1
  44. package/dist/server/handlers/handler-utils.js +5 -0
  45. package/dist/server/handlers/handler-utils.js.map +1 -1
  46. package/dist/server/handlers/messages-handler.js +13 -1
  47. package/dist/server/handlers/messages-handler.js.map +1 -1
  48. package/dist/server/handlers/responses-handler.js +73 -1
  49. package/dist/server/handlers/responses-handler.js.map +1 -1
  50. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
  51. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  52. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
  53. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  54. package/dist/server/runtime/http-server/executor-response.js +6 -0
  55. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  56. package/dist/server/runtime/http-server/index.d.ts +5 -0
  57. package/dist/server/runtime/http-server/index.js +205 -4
  58. package/dist/server/runtime/http-server/index.js.map +1 -1
  59. package/dist/server/runtime/http-server/middleware.d.ts +2 -0
  60. package/dist/server/runtime/http-server/middleware.js +63 -0
  61. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  62. package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
  63. package/dist/server/runtime/http-server/request-executor.js +57 -10
  64. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  65. package/dist/server/runtime/http-server/routes.js +38 -1
  66. package/dist/server/runtime/http-server/routes.js.map +1 -1
  67. package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
  68. package/dist/server/runtime/http-server/stats-manager.js +462 -4
  69. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  70. package/dist/server/runtime/http-server/types.d.ts +1 -0
  71. package/dist/token-daemon/token-daemon.d.ts +3 -1
  72. package/dist/token-daemon/token-daemon.js +130 -8
  73. package/dist/token-daemon/token-daemon.js.map +1 -1
  74. package/dist/token-daemon/token-utils.d.ts +1 -0
  75. package/dist/token-daemon/token-utils.js +9 -1
  76. package/dist/token-daemon/token-utils.js.map +1 -1
  77. package/dist/tools/semantic-replay.js +29 -0
  78. package/dist/tools/semantic-replay.js.map +1 -1
  79. package/dist/utils/snapshot-writer.d.ts +2 -0
  80. package/dist/utils/snapshot-writer.js +47 -4
  81. package/dist/utils/snapshot-writer.js.map +1 -1
  82. package/package.json +2 -3
  83. package/scripts/analyze-apply-patch-exec-failures.mjs +153 -0
  84. package/scripts/analyze-apply-patch-samples.mjs +242 -0
  85. package/scripts/analyze-codex-error-failures.mjs +24 -14
  86. package/scripts/classify-codex-samples.mjs +0 -35
  87. package/scripts/copy-modules-config.mjs +17 -1
  88. package/scripts/generate-snapshot-data.mjs +41 -11
  89. package/scripts/mock-provider/extract.mjs +254 -21
  90. package/scripts/mock-provider/run-regressions.mjs +97 -16
  91. package/scripts/quota-dryrun.mjs +124 -0
  92. package/scripts/tests/apply-patch-loop.mjs +5 -1
  93. package/scripts/tests/exec-command-loop.mjs +16 -19
  94. package/scripts/verify-apply-patch.mjs +335 -5
  95. package/scripts/verify-e2e-toolcall.mjs +49 -10
  96. package/scripts/toon-suite.mjs +0 -141
@@ -105,8 +105,8 @@ function findProviderId(node) {
105
105
  }
106
106
 
107
107
  async function detectProviderId(entryDir, prefix, request) {
108
- const semanticPath = path.join(entryDir, `${prefix}_semantic_map_from_chat.json`);
109
- if (await fileExists(semanticPath)) {
108
+ const semanticPath = prefix ? path.join(entryDir, `${prefix}_semantic_map_from_chat.json`) : null;
109
+ if (semanticPath && (await fileExists(semanticPath))) {
110
110
  try {
111
111
  const semantic = JSON.parse(await fs.readFile(semanticPath, 'utf-8'));
112
112
  const detected = findProviderId(semantic);
@@ -119,24 +119,100 @@ async function detectProviderId(entryDir, prefix, request) {
119
119
  }
120
120
  const fallback =
121
121
  request?.providerId ||
122
- request?.data?.providerId ||
123
- request?.data?.meta?.providerId ||
122
+ request?.meta?.providerId ||
123
+ request?.body?.providerId ||
124
+ request?.body?.meta?.providerId ||
124
125
  'unknown';
125
126
  return sanitizeComponent(fallback, 'unknown');
126
127
  }
127
128
 
128
129
  function buildTags(response) {
129
130
  const tags = [];
130
- const body = response?.data?.body;
131
- if (body?.__sse_responses) {
131
+ const body = response?.body || response?.data?.body;
132
+ if (body?.__sse_responses || body?.mode === 'sse') {
132
133
  tags.push('sse');
133
134
  }
134
- if (Array.isArray(body?.tool_calls) && body.tool_calls.length > 0) {
135
+ const toolCalls = body?.tool_calls || body?.output?.tool_calls;
136
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
135
137
  tags.push('tool-call');
136
138
  }
137
139
  return tags;
138
140
  }
139
141
 
142
+ function parseRequestTimestamp(requestId, request) {
143
+ if (typeof request?.timestamp === 'number' && Number.isFinite(request.timestamp)) {
144
+ return request.timestamp;
145
+ }
146
+ const buildTime = request?.meta?.buildTime;
147
+ if (typeof buildTime === 'string') {
148
+ const parsed = Date.parse(buildTime);
149
+ if (!Number.isNaN(parsed)) {
150
+ return parsed;
151
+ }
152
+ }
153
+ const match = String(requestId || '').match(/(\d{8})T(\d{6})(\d{3})?/);
154
+ if (match) {
155
+ const ymd = match[1];
156
+ const hms = match[2];
157
+ const ms = match[3] || '000';
158
+ const year = Number(ymd.slice(0, 4));
159
+ const month = Number(ymd.slice(4, 6));
160
+ const day = Number(ymd.slice(6, 8));
161
+ const hour = Number(hms.slice(0, 2));
162
+ const min = Number(hms.slice(2, 4));
163
+ const sec = Number(hms.slice(4, 6));
164
+ const msNum = Number(ms);
165
+ if ([year, month, day, hour, min, sec, msNum].every((v) => Number.isFinite(v))) {
166
+ return new Date(year, month - 1, day, hour, min, sec, msNum).getTime();
167
+ }
168
+ }
169
+ return Date.now();
170
+ }
171
+
172
+ function extractModelFromProviderRequest(request) {
173
+ const body = request?.body || request?.data?.body;
174
+ const raw =
175
+ body?.model ||
176
+ body?.data?.model ||
177
+ request?.model ||
178
+ request?.data?.model ||
179
+ undefined;
180
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : 'unknown';
181
+ }
182
+
183
+ async function detectProviderIdFromRequestDir(requestDir, request) {
184
+ try {
185
+ const files = await fs.readdir(requestDir);
186
+ const semanticCandidates = files
187
+ .filter((name) => name.toLowerCase().endsWith('.json') && name.toLowerCase().includes('semantic_map'))
188
+ .slice(0, 10);
189
+ for (const file of semanticCandidates) {
190
+ try {
191
+ const semantic = JSON.parse(await fs.readFile(path.join(requestDir, file), 'utf-8'));
192
+ const detected = findProviderId(semantic);
193
+ if (detected) {
194
+ return sanitizeComponent(detected, 'unknown');
195
+ }
196
+ } catch {
197
+ // ignore semantic parse errors
198
+ }
199
+ }
200
+ } catch {
201
+ // ignore dir read errors
202
+ }
203
+ return await detectProviderId(requestDir, null, request);
204
+ }
205
+
206
+ function pickLatestStageFile(files, prefix) {
207
+ const candidates = files
208
+ .filter((name) => name.toLowerCase().endsWith('.json') && name.startsWith(prefix))
209
+ .sort((a, b) => a.localeCompare(b, 'en'));
210
+ if (!candidates.length) {
211
+ return null;
212
+ }
213
+ return candidates[candidates.length - 1];
214
+ }
215
+
140
216
  async function extractPair(entryName, entryDir, file, registry, seqMap, filters = {}) {
141
217
  const prefix = file.replace('_provider-request.json', '');
142
218
  const requestPath = path.join(entryDir, file);
@@ -152,8 +228,8 @@ async function extractPair(entryName, entryDir, file, registry, seqMap, filters
152
228
  if (filters.provider && providerId !== sanitizeComponent(filters.provider, filters.provider)) {
153
229
  return;
154
230
  }
155
- const model = sanitizeComponent(request?.data?.body?.model, 'unknown');
156
- const tsToken = timestampToToken(request?.timestamp);
231
+ const model = sanitizeComponent(extractModelFromProviderRequest(request), 'unknown');
232
+ const tsToken = timestampToToken(parseRequestTimestamp(prefix, request));
157
233
  const seqKey = `${entryName}|${providerId}|${model}|${tsToken}`;
158
234
  const seq = (seqMap.get(seqKey) || 0) + 1;
159
235
  seqMap.set(seqKey, seq);
@@ -170,6 +246,21 @@ async function extractPair(entryName, entryDir, file, registry, seqMap, filters
170
246
  await fs.writeFile(path.join(targetDir, 'request.json'), JSON.stringify(enrichedRequest, null, 2));
171
247
  await fs.writeFile(path.join(targetDir, 'response.json'), JSON.stringify(enrichedResponse, null, 2));
172
248
 
249
+ // 可选:如果存在入口层的 client-request 快照,一并抽取,便于端到端重放。
250
+ const clientSnapshotPath = path.join(entryDir, `${prefix}_client-request.json`);
251
+ if (await fileExists(clientSnapshotPath)) {
252
+ try {
253
+ const client = JSON.parse(await fs.readFile(clientSnapshotPath, 'utf-8'));
254
+ const enrichedClient = { ...client, reqId, entryEndpoint: client?.endpoint };
255
+ await fs.writeFile(
256
+ path.join(targetDir, 'client-request.json'),
257
+ JSON.stringify(enrichedClient, null, 2)
258
+ );
259
+ } catch {
260
+ // client-request 仅用于重放,解析失败不阻断样本注册。
261
+ }
262
+ }
263
+
173
264
  registry.samples = registry.samples.filter((sample) => sample.reqId !== reqId);
174
265
  registry.samples.push({
175
266
  reqId,
@@ -183,6 +274,66 @@ async function extractPair(entryName, entryDir, file, registry, seqMap, filters
183
274
  console.log(`✅ ${reqId}`);
184
275
  }
185
276
 
277
+ async function extractPairFromRequestDir(entryName, requestDir, registry, seqMap, filters = {}) {
278
+ const files = await fs.readdir(requestDir);
279
+ const providerRequestFile = pickLatestStageFile(files, 'provider-request');
280
+ const providerResponseFile = pickLatestStageFile(files, 'provider-response');
281
+ if (!providerRequestFile || !providerResponseFile) {
282
+ return;
283
+ }
284
+
285
+ const requestPath = path.join(requestDir, providerRequestFile);
286
+ const responsePath = path.join(requestDir, providerResponseFile);
287
+ const request = JSON.parse(await fs.readFile(requestPath, 'utf-8'));
288
+ const response = JSON.parse(await fs.readFile(responsePath, 'utf-8'));
289
+
290
+ const requestId = path.basename(requestDir);
291
+ const providerId = await detectProviderIdFromRequestDir(requestDir, request);
292
+ if (filters.provider && providerId !== sanitizeComponent(filters.provider, filters.provider)) {
293
+ return;
294
+ }
295
+ const model = sanitizeComponent(extractModelFromProviderRequest(request), 'unknown');
296
+ const tsToken = timestampToToken(parseRequestTimestamp(requestId, request));
297
+ const seqKey = `${entryName}|${providerId}|${model}|${tsToken}`;
298
+ const seq = (seqMap.get(seqKey) || 0) + 1;
299
+ seqMap.set(seqKey, seq);
300
+ const seqStr = String(seq).padStart(3, '0');
301
+ const reqId = `${entryName}-${providerId}-${model}-${tsToken}-${seqStr}`;
302
+
303
+ const daySegment = tsToken.slice(0, 8);
304
+ const timeSegment = tsToken.slice(9);
305
+ const targetDir = path.join(MOCK_SAMPLES_DIR, entryName, providerId, model, daySegment, timeSegment, seqStr);
306
+ await ensureDir(targetDir);
307
+
308
+ const enrichedRequest = { ...request, reqId, entryEndpoint: request?.endpoint };
309
+ const enrichedResponse = { ...response, reqId, entryEndpoint: response?.endpoint };
310
+ await fs.writeFile(path.join(targetDir, 'request.json'), JSON.stringify(enrichedRequest, null, 2));
311
+ await fs.writeFile(path.join(targetDir, 'response.json'), JSON.stringify(enrichedResponse, null, 2));
312
+
313
+ const clientRequestFile = pickLatestStageFile(files, 'client-request');
314
+ if (clientRequestFile) {
315
+ try {
316
+ const client = JSON.parse(await fs.readFile(path.join(requestDir, clientRequestFile), 'utf-8'));
317
+ const enrichedClient = { ...client, reqId, entryEndpoint: client?.endpoint };
318
+ await fs.writeFile(path.join(targetDir, 'client-request.json'), JSON.stringify(enrichedClient, null, 2));
319
+ } catch {
320
+ // ignore client-request parse errors
321
+ }
322
+ }
323
+
324
+ registry.samples = registry.samples.filter((sample) => sample.reqId !== reqId);
325
+ registry.samples.push({
326
+ reqId,
327
+ entry: entryName,
328
+ providerId,
329
+ model,
330
+ timestamp: new Date(parseRequestTimestamp(requestId, request)).toISOString(),
331
+ path: path.relative(MOCK_SAMPLES_DIR, targetDir),
332
+ tags: buildTags(response)
333
+ });
334
+ console.log(`✅ ${reqId}`);
335
+ }
336
+
186
337
  async function extractAll(options = {}) {
187
338
  console.log('[mock:extract] Preparing directories...');
188
339
  await ensureDir(MOCK_SAMPLES_DIR);
@@ -199,7 +350,41 @@ async function extractAll(options = {}) {
199
350
  continue;
200
351
  }
201
352
  const entryDir = path.join(CODEX_SAMPLES_DIR, dirEntry.name);
202
- const files = await fs.readdir(entryDir);
353
+ const entries = await fs.readdir(entryDir, { withFileTypes: true });
354
+
355
+ // Newer layout: entry/<provider>/<requestId>/... (all stages in one directory)
356
+ // Previous layout: entry/<requestId>/... (single-level request directory)
357
+ for (const sub of entries) {
358
+ if (!sub.isDirectory()) continue;
359
+ const maybeProviderOrRequestDir = path.join(entryDir, sub.name);
360
+ let nested = [];
361
+ try {
362
+ nested = await fs.readdir(maybeProviderOrRequestDir, { withFileTypes: true });
363
+ } catch {
364
+ nested = [];
365
+ }
366
+ const hasJsonFiles = nested.some((e) => e.isFile() && e.name.toLowerCase().endsWith('.json'));
367
+ if (hasJsonFiles) {
368
+ try {
369
+ await extractPairFromRequestDir(dirEntry.name, maybeProviderOrRequestDir, registry, seqMap, { provider: providerFilter });
370
+ } catch (error) {
371
+ console.warn(`⚠️ Skipped ${sub.name}: ${error.message}`);
372
+ }
373
+ continue;
374
+ }
375
+ const requestDirs = nested.filter((e) => e.isDirectory());
376
+ for (const reqSub of requestDirs) {
377
+ const requestDir = path.join(maybeProviderOrRequestDir, reqSub.name);
378
+ try {
379
+ await extractPairFromRequestDir(dirEntry.name, requestDir, registry, seqMap, { provider: providerFilter });
380
+ } catch (error) {
381
+ console.warn(`⚠️ Skipped ${sub.name}/${reqSub.name}: ${error.message}`);
382
+ }
383
+ }
384
+ }
385
+
386
+ // Legacy layout: entry/*_provider-request.json
387
+ const files = entries.filter((e) => e.isFile()).map((e) => e.name);
203
388
  const requests = files.filter((name) => name.endsWith('_provider-request.json'));
204
389
  for (const file of requests) {
205
390
  try {
@@ -215,25 +400,73 @@ async function extractAll(options = {}) {
215
400
  }
216
401
 
217
402
  async function extractSingle(reqId) {
218
- const parts = reqId.split('-');
219
- if (parts.length < 5) {
220
- throw new Error(`Invalid requestId format: ${reqId}`);
403
+ await ensureDir(MOCK_SAMPLES_DIR);
404
+ await ensureDir(path.join(MOCK_SAMPLES_DIR, '_registry'));
405
+ const registry = await loadRegistry();
406
+ const seqMap = buildSeqMapFromRegistry(registry);
407
+
408
+ const inferredEntry = inferEntryFolder(reqId);
409
+ const entryDir = path.join(CODEX_SAMPLES_DIR, inferredEntry);
410
+ const requestDir = path.join(entryDir, sanitizeRequestDirName(reqId));
411
+ try {
412
+ const stat = await fs.stat(requestDir);
413
+ if (stat.isDirectory()) {
414
+ await extractPairFromRequestDir(inferredEntry, requestDir, registry, seqMap);
415
+ await saveRegistry(registry);
416
+ return;
417
+ }
418
+ } catch {
419
+ // fall through
221
420
  }
222
- const entry = parts.slice(0, parts.length - 4).join('-') || parts[0];
223
- const entryDir = path.join(CODEX_SAMPLES_DIR, entry);
421
+
422
+ // Newer layout: entry/<provider>/<requestId>/...
423
+ try {
424
+ const providerDirs = await fs.readdir(entryDir, { withFileTypes: true });
425
+ for (const providerDir of providerDirs) {
426
+ if (!providerDir.isDirectory()) continue;
427
+ const candidate = path.join(entryDir, providerDir.name, sanitizeRequestDirName(reqId));
428
+ try {
429
+ const stat = await fs.stat(candidate);
430
+ if (stat.isDirectory()) {
431
+ await extractPairFromRequestDir(inferredEntry, candidate, registry, seqMap);
432
+ await saveRegistry(registry);
433
+ return;
434
+ }
435
+ } catch {
436
+ // continue
437
+ }
438
+ }
439
+ } catch {
440
+ // ignore entryDir scan errors
441
+ }
442
+
224
443
  const files = await fs.readdir(entryDir);
225
444
  const target = files.find((file) => file.includes(reqId) && file.endsWith('_provider-request.json'));
226
445
  if (!target) {
227
- throw new Error(`Request ${reqId} not found under ${entry}`);
446
+ throw new Error(`Request ${reqId} not found under ${inferredEntry}`);
228
447
  }
229
- await ensureDir(MOCK_SAMPLES_DIR);
230
- await ensureDir(path.join(MOCK_SAMPLES_DIR, '_registry'));
231
- const registry = await loadRegistry();
232
- const seqMap = buildSeqMapFromRegistry(registry);
233
- await extractPair(entry, entryDir, target, registry, seqMap);
448
+ await extractPair(inferredEntry, entryDir, target, registry, seqMap);
234
449
  await saveRegistry(registry);
235
450
  }
236
451
 
452
+ function sanitizeRequestDirName(value) {
453
+ if (typeof value !== 'string' || !value.trim()) {
454
+ return `req_${Date.now()}`;
455
+ }
456
+ return value.trim().replace(/[^A-Za-z0-9_.-]/g, '_');
457
+ }
458
+
459
+ function inferEntryFolder(reqId) {
460
+ const lower = String(reqId || '').toLowerCase();
461
+ if (lower.includes('openai-responses') || lower.includes('responses')) {
462
+ return 'openai-responses';
463
+ }
464
+ if (lower.includes('anthropic')) {
465
+ return 'anthropic-messages';
466
+ }
467
+ return 'openai-chat';
468
+ }
469
+
237
470
  function parseArguments(argv) {
238
471
  const opts = { mode: 'all', reqId: undefined, provider: undefined, entry: undefined };
239
472
  for (const arg of argv) {
@@ -37,13 +37,14 @@ function parseEntryFilter() {
37
37
 
38
38
  async function ensureCliAvailable() {
39
39
  const cliPath = path.join(PROJECT_ROOT, 'dist', 'cli.js');
40
- if (await fileExists(cliPath)) {
40
+ const serverPath = path.join(PROJECT_ROOT, 'dist', 'index.js');
41
+ if ((await fileExists(cliPath)) && (await fileExists(serverPath))) {
41
42
  return;
42
43
  }
43
- console.warn('[mock:regressions] dist/cli.js missing, running "npm run build:min" automatically...');
44
+ console.warn('[mock:regressions] dist artifacts missing (cli.js/index.js), running "npm run build:min" automatically...');
44
45
  await runBuildForMockRegressions();
45
- if (!(await fileExists(cliPath))) {
46
- throw new Error('dist/cli.js missing after automatic build. Please run "npm run build:dev" manually.');
46
+ if (!(await fileExists(cliPath)) || !(await fileExists(serverPath))) {
47
+ throw new Error('dist artifacts missing after automatic build. Please run "npm run build:dev" manually.');
47
48
  }
48
49
  }
49
50
 
@@ -187,7 +188,7 @@ async function writeTempConfig(sample, port) {
187
188
  return { dir, file };
188
189
  }
189
190
 
190
- function createServer(configPath, port) {
191
+ function createServer(configPath, port, snapshotRoot) {
191
192
  const env = {
192
193
  ...process.env,
193
194
  ROUTECODEX_USE_MOCK: '1',
@@ -196,7 +197,14 @@ function createServer(configPath, port) {
196
197
  ROUTECODEX_MOCK_VALIDATE_NAMES: '1',
197
198
  ROUTECODEX_STAGE_LOG: process.env.ROUTECODEX_STAGE_LOG ?? '0',
198
199
  ROUTECODEX_PORT: String(port),
199
- ROUTECODEX_CONFIG_PATH: configPath
200
+ ROUTECODEX_CONFIG_PATH: configPath,
201
+ // 将快照写入临时目录,避免污染全局 ~/.routecodex/codex-samples 样本
202
+ ...(snapshotRoot
203
+ ? {
204
+ ROUTECODEX_SNAPSHOT_DIR: snapshotRoot,
205
+ RCC_SNAPSHOT_DIR: snapshotRoot
206
+ }
207
+ : {})
200
208
  };
201
209
  const entry = path.join(PROJECT_ROOT, 'dist', 'index.js');
202
210
  const child = spawn(process.execPath, [entry], {
@@ -305,10 +313,9 @@ function validateToolCallIds(payload, sample, tagSet) {
305
313
  if (!payload || typeof payload !== 'object') {
306
314
  return errors;
307
315
  }
308
- const wantsFcIds =
309
- tagSet.has('require_fc_call_ids') ||
310
- tagSet.has('missing_tool_call_id') ||
311
- tagSet.has('regression');
316
+ const enforceCallIdFormat = tagSet.has('require_fc_call_ids');
317
+ const isValidCallId = (value) =>
318
+ /^call_[A-Za-z0-9]+$/.test(value) || /^fc[_-][A-Za-z0-9-]+$/.test(value);
312
319
 
313
320
  const allToolCallIds = new Set();
314
321
  if (Array.isArray(payload.output)) {
@@ -323,7 +330,7 @@ function validateToolCallIds(payload, sample, tagSet) {
323
330
  return;
324
331
  }
325
332
  allToolCallIds.add(rawId);
326
- if (wantsFcIds && !/^call_[A-Za-z0-9]+$/.test(rawId)) {
333
+ if (enforceCallIdFormat && !isValidCallId(rawId)) {
327
334
  errors.push(`output[${oi}].tool_calls[${ti}].id has invalid format: ${rawId}`);
328
335
  }
329
336
  });
@@ -340,12 +347,16 @@ function validateToolCallIds(payload, sample, tagSet) {
340
347
  if (Array.isArray(submitCalls)) {
341
348
  submitCalls.forEach((tc, i) => {
342
349
  if (!tc || typeof tc !== 'object') return;
343
- const rawId = typeof tc.tool_call_id === 'string' ? tc.tool_call_id.trim() : '';
350
+ const rawId = typeof tc.tool_call_id === 'string'
351
+ ? tc.tool_call_id.trim()
352
+ : typeof tc.id === 'string'
353
+ ? tc.id.trim()
354
+ : '';
344
355
  if (!rawId) {
345
356
  errors.push(`required_action.submit_tool_outputs.tool_calls[${i}].tool_call_id missing`);
346
357
  return;
347
358
  }
348
- if (wantsFcIds && !/^call_[A-Za-z0-9]+$/.test(rawId)) {
359
+ if (enforceCallIdFormat && !isValidCallId(rawId)) {
349
360
  errors.push(
350
361
  `required_action.submit_tool_outputs.tool_calls[${i}].tool_call_id has invalid format: ${rawId}`
351
362
  );
@@ -395,6 +406,10 @@ function resolveRequestUrl(sample, requestDoc, port) {
395
406
  async function sendRequest(sample, requestDoc, port) {
396
407
  const url = resolveRequestUrl(sample, requestDoc, port);
397
408
  const payload = extractRequestBody(requestDoc);
409
+ if (payload && typeof payload === 'object') {
410
+ const meta = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {};
411
+ payload.metadata = { ...meta, mockSampleReqId: sample.reqId };
412
+ }
398
413
  const headers = { 'content-type': 'application/json' };
399
414
  const wantsStream =
400
415
  payload?.stream === true ||
@@ -424,15 +439,64 @@ async function sendRequest(sample, requestDoc, port) {
424
439
  }
425
440
  }
426
441
 
442
+ function looksLikeSseErrorStream(text) {
443
+ if (typeof text !== 'string') {
444
+ return false;
445
+ }
446
+ const trimmed = text.trim();
447
+ if (!trimmed) {
448
+ return false;
449
+ }
450
+ // 简单判定:包含 SSE error 事件头和 JSON error 负载。
451
+ if (trimmed.includes('event: error') && trimmed.includes('data:')) {
452
+ return true;
453
+ }
454
+ if (trimmed.includes('"type":"error"') || trimmed.includes('"status":502')) {
455
+ return true;
456
+ }
457
+ return false;
458
+ }
459
+
427
460
  async function runSample(sample, index) {
428
461
  const clientDoc = await loadSampleDocument(sample, { fileName: 'client-request.json', optional: true });
429
462
  const requestDoc = clientDoc || (await loadSampleDocument(sample));
463
+ const responseDoc = await loadSampleDocument(sample, { fileName: 'response.json', optional: true });
430
464
  const port = 5800 + index;
431
465
  const { dir, file } = await writeTempConfig(sample, port);
432
- const server = createServer(file, port);
466
+ // 为当前样本创建独立的临时快照根目录,并在完成后整体删除
467
+ const snapshotRoot = path.join(dir, 'codex-samples');
468
+ const server = createServer(file, port, snapshotRoot);
469
+ const tags = new Set(Array.isArray(sample.tags) ? sample.tags : []);
470
+ const expectSseTerminationError = tags.has('responses_sse_terminated');
471
+ const allowSampleError =
472
+ responseDoc && typeof responseDoc.status === 'number' && responseDoc.status >= 400;
433
473
  try {
434
474
  await waitForHealth(port, server.process);
435
- const responseText = await sendRequest(sample, requestDoc, port);
475
+ let responseText;
476
+ try {
477
+ responseText = await sendRequest(sample, requestDoc, port);
478
+ if (expectSseTerminationError) {
479
+ if (!looksLikeSseErrorStream(responseText)) {
480
+ throw new Error(
481
+ 'expected SSE termination to surface as HTTP error or SSE error event, but got successful non-error payload'
482
+ );
483
+ }
484
+ // 对于 SSE 终止样本,只要以 SSE error 事件形式返回即可视为通过。
485
+ return;
486
+ }
487
+ } catch (sendError) {
488
+ if (expectSseTerminationError || allowSampleError) {
489
+ const msg = sendError instanceof Error ? sendError.message : String(sendError);
490
+ if (!/HTTP\s+4\d\d|HTTP\s+5\d\d/i.test(msg)) {
491
+ throw new Error(
492
+ `expected HTTP 4xx/5xx error, but got: ${msg}`
493
+ );
494
+ }
495
+ // 对于错误类样本,只要成功以 HTTP 错误形式透出即可。
496
+ return;
497
+ }
498
+ throw sendError;
499
+ }
436
500
  const body = (() => {
437
501
  try {
438
502
  return JSON.parse(responseText);
@@ -440,7 +504,6 @@ async function runSample(sample, index) {
440
504
  return undefined;
441
505
  }
442
506
  })();
443
- const tags = new Set(Array.isArray(sample.tags) ? sample.tags : []);
444
507
  if (body && Array.isArray(body.output)) {
445
508
  const invalid = collectInvalidNames(body);
446
509
  if (invalid.length) {
@@ -482,6 +545,10 @@ async function main() {
482
545
  console.warn('[mock:regressions] No regression-tagged samples matched current filters; skipping mock replay.');
483
546
  return;
484
547
  }
548
+
549
+ const coverageByEntry = Object.create(null);
550
+ const coverageByProvider = Object.create(null);
551
+
485
552
  const tagCounter = Object.create(null);
486
553
  const incrementTag = (tag) => {
487
554
  if (!watchedTags.has(tag)) {
@@ -491,6 +558,12 @@ async function main() {
491
558
  };
492
559
  regressionSamples.forEach((sample) => {
493
560
  const matched = new Set();
561
+ const entry = typeof sample.entry === 'string' && sample.entry.trim().length ? sample.entry.trim() : 'unknown';
562
+ const providerId =
563
+ typeof sample.providerId === 'string' && sample.providerId.trim().length ? sample.providerId.trim() : 'unknown';
564
+ coverageByEntry[entry] = (coverageByEntry[entry] || 0) + 1;
565
+ coverageByProvider[providerId] = (coverageByProvider[providerId] || 0) + 1;
566
+
494
567
  (sample.tags || []).forEach((tag) => {
495
568
  if (watchedTags.has(tag) && !matched.has(tag)) {
496
569
  incrementTag(tag);
@@ -506,6 +579,14 @@ async function main() {
506
579
  .map((tag) => `${tag}=${tagCounter[tag] || 0}`)
507
580
  .join(', ');
508
581
  console.log(`✅ mock provider regressions passed (${regressionSamples.length} samples · ${summary})`);
582
+ const byEntry = Object.entries(coverageByEntry)
583
+ .map(([entry, count]) => `${entry}=${count}`)
584
+ .join(', ');
585
+ const byProvider = Object.entries(coverageByProvider)
586
+ .map(([pid, count]) => `${pid}=${count}`)
587
+ .join(', ');
588
+ console.log(`[mock:regressions] coverage by entry: ${byEntry}`);
589
+ console.log(`[mock:regressions] coverage by providerId: ${byProvider}`);
509
590
  }
510
591
 
511
592
  main().catch((error) => {
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Quota dry-run helper:
5
+ * 从简单的 JSON 事件数组读取错误/成功/usage 事件,驱动 provider-quota-center,
6
+ * 并将结果写入 ~/.routecodex/quota/provider-quota.json,方便人工检查。
7
+ *
8
+ * 用法:
9
+ * node scripts/quota-dryrun.mjs path/to/events.json
10
+ *
11
+ * 事件格式示例:
12
+ * [
13
+ * { "type": "error", "providerKey": "antigravity.alias1.gemini-3-pro-high", "httpStatus": 429 },
14
+ * { "type": "success", "providerKey": "antigravity.alias1.gemini-3-pro-high", "usedTokens": 120 },
15
+ * { "type": "usage", "providerKey": "antigravity.alias1.gemini-3-pro-high", "requestedTokens": 80 }
16
+ * ]
17
+ */
18
+
19
+ import fs from 'node:fs/promises';
20
+ import path from 'node:path';
21
+ import {
22
+ applyErrorEvent,
23
+ applySuccessEvent,
24
+ applyUsageEvent,
25
+ createInitialQuotaState
26
+ } from '../src/manager/quota/provider-quota-center.js';
27
+ import {
28
+ saveProviderQuotaSnapshot
29
+ } from '../src/manager/quota/provider-quota-store.js';
30
+
31
+ async function main() {
32
+ const fileArg = process.argv[2];
33
+ if (!fileArg) {
34
+ // eslint-disable-next-line no-console
35
+ console.error('Usage: node scripts/quota-dryrun.mjs path/to/events.json');
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const filePath = path.resolve(process.cwd(), fileArg);
40
+ const raw = await fs.readFile(filePath, 'utf8');
41
+ const events = JSON.parse(raw);
42
+ if (!Array.isArray(events)) {
43
+ throw new Error('events file must contain a JSON array');
44
+ }
45
+
46
+ const states = new Map();
47
+ const nowMs = Date.now();
48
+
49
+ for (const entry of events) {
50
+ if (!entry || typeof entry !== 'object') {
51
+ // eslint-disable-next-line no-console
52
+ console.warn('[quota-dryrun] skip non-object event', entry);
53
+ continue;
54
+ }
55
+ const record = entry;
56
+ const providerKey = typeof record.providerKey === 'string' ? record.providerKey.trim() : '';
57
+ if (!providerKey) {
58
+ // eslint-disable-next-line no-console
59
+ console.warn('[quota-dryrun] event missing providerKey', record);
60
+ continue;
61
+ }
62
+ const type = typeof record.type === 'string' ? record.type.trim().toLowerCase() : '';
63
+ if (!type) {
64
+ // eslint-disable-next-line no-console
65
+ console.warn('[quota-dryrun] event missing type', record);
66
+ continue;
67
+ }
68
+ const existing = states.get(providerKey) as any | undefined;
69
+ const baseState =
70
+ existing ??
71
+ createInitialQuotaState(providerKey, undefined, nowMs);
72
+ let nextState = baseState;
73
+
74
+ if (type === 'error') {
75
+ nextState = applyErrorEvent(
76
+ baseState,
77
+ {
78
+ providerKey,
79
+ code: record.code,
80
+ httpStatus: record.httpStatus,
81
+ fatal: record.fatal === true
82
+ },
83
+ nowMs
84
+ );
85
+ } else if (type === 'success') {
86
+ nextState = applySuccessEvent(
87
+ baseState,
88
+ {
89
+ providerKey,
90
+ usedTokens: record.usedTokens
91
+ },
92
+ nowMs
93
+ );
94
+ } else if (type === 'usage') {
95
+ nextState = applyUsageEvent(
96
+ baseState,
97
+ {
98
+ providerKey,
99
+ requestedTokens: record.requestedTokens
100
+ },
101
+ nowMs
102
+ );
103
+ } else {
104
+ // eslint-disable-next-line no-console
105
+ console.warn('[quota-dryrun] unknown event type', type);
106
+ continue;
107
+ }
108
+ states.set(providerKey, nextState);
109
+ }
110
+
111
+ const snapshot = Object.fromEntries(states.entries());
112
+ await saveProviderQuotaSnapshot(snapshot, new Date());
113
+ // eslint-disable-next-line no-console
114
+ console.log(
115
+ `[quota-dryrun] wrote snapshot for ${states.size} provider(s) to ~/.routecodex/quota/provider-quota.json`
116
+ );
117
+ }
118
+
119
+ main().catch((error) => {
120
+ // eslint-disable-next-line no-console
121
+ console.error('[quota-dryrun] failed:', error);
122
+ process.exitCode = 1;
123
+ });
124
+