@jsonstudio/rcc 0.89.942 → 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 (91) 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 +106 -10
  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/docs/daemon-admin-ui.html +958 -0
  10. package/dist/index.js +5 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/manager/modules/quota/index.d.ts +34 -0
  13. package/dist/manager/modules/quota/index.js +291 -0
  14. package/dist/manager/modules/quota/index.js.map +1 -1
  15. package/dist/manager/modules/token/index.js +13 -2
  16. package/dist/manager/modules/token/index.js.map +1 -1
  17. package/dist/manager/quota/provider-quota-center.d.ts +48 -0
  18. package/dist/manager/quota/provider-quota-center.js +239 -0
  19. package/dist/manager/quota/provider-quota-center.js.map +1 -0
  20. package/dist/manager/quota/provider-quota-store.d.ts +17 -0
  21. package/dist/manager/quota/provider-quota-store.js +88 -0
  22. package/dist/manager/quota/provider-quota-store.js.map +1 -0
  23. package/dist/providers/auth/token-scanner/index.js +11 -3
  24. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  25. package/dist/providers/core/runtime/http-request-executor.js +24 -7
  26. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  27. package/dist/providers/core/runtime/http-transport-provider.js +11 -3
  28. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  29. package/dist/providers/core/runtime/responses-provider.js +9 -3
  30. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  31. package/dist/providers/core/utils/http-client.d.ts +1 -0
  32. package/dist/providers/core/utils/http-client.js +139 -4
  33. package/dist/providers/core/utils/http-client.js.map +1 -1
  34. package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
  35. package/dist/providers/core/utils/snapshot-writer.js +99 -18
  36. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  37. package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
  38. package/dist/providers/mock/mock-provider-runtime.js +176 -4
  39. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  40. package/dist/server/handlers/chat-handler.js +13 -1
  41. package/dist/server/handlers/chat-handler.js.map +1 -1
  42. package/dist/server/handlers/handler-utils.js +5 -0
  43. package/dist/server/handlers/handler-utils.js.map +1 -1
  44. package/dist/server/handlers/messages-handler.js +13 -1
  45. package/dist/server/handlers/messages-handler.js.map +1 -1
  46. package/dist/server/handlers/responses-handler.js +73 -1
  47. package/dist/server/handlers/responses-handler.js.map +1 -1
  48. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
  49. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  50. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
  51. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  52. package/dist/server/runtime/http-server/executor-response.js +6 -0
  53. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  54. package/dist/server/runtime/http-server/index.d.ts +5 -0
  55. package/dist/server/runtime/http-server/index.js +205 -4
  56. package/dist/server/runtime/http-server/index.js.map +1 -1
  57. package/dist/server/runtime/http-server/middleware.d.ts +2 -0
  58. package/dist/server/runtime/http-server/middleware.js +63 -0
  59. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  60. package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
  61. package/dist/server/runtime/http-server/request-executor.js +57 -10
  62. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  63. package/dist/server/runtime/http-server/routes.js +38 -1
  64. package/dist/server/runtime/http-server/routes.js.map +1 -1
  65. package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
  66. package/dist/server/runtime/http-server/stats-manager.js +462 -4
  67. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  68. package/dist/server/runtime/http-server/types.d.ts +1 -0
  69. package/dist/token-daemon/token-daemon.js +70 -25
  70. package/dist/token-daemon/token-daemon.js.map +1 -1
  71. package/dist/token-daemon/token-utils.d.ts +1 -0
  72. package/dist/token-daemon/token-utils.js +9 -1
  73. package/dist/token-daemon/token-utils.js.map +1 -1
  74. package/dist/tools/semantic-replay.js +29 -0
  75. package/dist/tools/semantic-replay.js.map +1 -1
  76. package/dist/utils/snapshot-writer.d.ts +2 -0
  77. package/dist/utils/snapshot-writer.js +47 -4
  78. package/dist/utils/snapshot-writer.js.map +1 -1
  79. package/package.json +2 -3
  80. package/scripts/analyze-codex-error-failures.mjs +24 -14
  81. package/scripts/classify-codex-samples.mjs +0 -35
  82. package/scripts/copy-modules-config.mjs +17 -1
  83. package/scripts/generate-snapshot-data.mjs +41 -11
  84. package/scripts/mock-provider/extract.mjs +239 -21
  85. package/scripts/mock-provider/run-regressions.mjs +79 -16
  86. package/scripts/quota-dryrun.mjs +124 -0
  87. package/scripts/tests/apply-patch-loop.mjs +5 -1
  88. package/scripts/tests/exec-command-loop.mjs +16 -19
  89. package/scripts/verify-apply-patch.mjs +335 -5
  90. package/scripts/verify-e2e-toolcall.mjs +49 -10
  91. package/scripts/toon-suite.mjs +0 -141
@@ -37,11 +37,25 @@ async function fileExists(p) {
37
37
  }
38
38
  }
39
39
 
40
- async function listJsonFiles(root) {
41
- const entries = await fs.readdir(root);
42
- return entries
43
- .filter((name) => name.toLowerCase().endsWith('.json'))
44
- .map((name) => path.join(root, name));
40
+ async function* walkJsonFiles(root) {
41
+ const stack = [root];
42
+ while (stack.length) {
43
+ const current = stack.pop();
44
+ let entries;
45
+ try {
46
+ entries = await fs.readdir(current, { withFileTypes: true });
47
+ } catch {
48
+ continue;
49
+ }
50
+ for (const entry of entries) {
51
+ const full = path.join(current, entry.name);
52
+ if (entry.isDirectory()) {
53
+ stack.push(full);
54
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
55
+ yield full;
56
+ }
57
+ }
58
+ }
45
59
  }
46
60
 
47
61
  async function analyzeFile(filePath) {
@@ -61,20 +75,13 @@ async function main() {
61
75
  process.exit(0);
62
76
  }
63
77
 
64
- const files = await listJsonFiles(RESPONSES_DIR);
65
- if (!files.length) {
66
- console.log('[analyze-codex-error-failures] no JSON files under', RESPONSES_DIR);
67
- process.exit(0);
68
- }
69
-
70
- console.log(`[analyze-codex-error-failures] scanning ${files.length} file(s) under ${RESPONSES_DIR}`);
71
-
72
78
  const summary = new Map();
73
79
  for (const p of PATTERNS) {
74
80
  summary.set(p, { count: 0, files: [] });
75
81
  }
76
82
 
77
- for (const file of files) {
83
+ let scanned = 0;
84
+ for await (const file of walkJsonFiles(RESPONSES_DIR)) {
78
85
  let res;
79
86
  try {
80
87
  res = await analyzeFile(file);
@@ -90,8 +97,11 @@ async function main() {
90
97
  entry.files.push(path.basename(file));
91
98
  }
92
99
  }
100
+ scanned += 1;
93
101
  }
94
102
 
103
+ console.log(`[analyze-codex-error-failures] scanned ${scanned} file(s) under ${RESPONSES_DIR}`);
104
+
95
105
  for (const key of PATTERNS) {
96
106
  const { count, files } = summary.get(key);
97
107
  console.log(`\n=== Pattern: "${key}" ===`);
@@ -30,7 +30,6 @@ const PROVIDER_KEYS = {
30
30
  const TOOL_TYPES = {
31
31
  'apply_patch': 'apply_patch',
32
32
  'shell': 'shell_command',
33
- 'toon': 'toon_tool',
34
33
  'submit_tool_outputs': 'tool_loop',
35
34
  'list_files': 'file_operation',
36
35
  'write_file': 'file_operation',
@@ -46,7 +45,6 @@ class SampleClassifier {
46
45
  byProvider: {},
47
46
  byToolType: {},
48
47
  withToolCalls: 0,
49
- withToon: 0,
50
48
  errors: 0
51
49
  };
52
50
  }
@@ -68,9 +66,6 @@ class SampleClassifier {
68
66
  identifyToolType(toolCall) {
69
67
  const funcName = toolCall.function?.name || '';
70
68
 
71
- // 检查 TOON 格式
72
- if (this.isToonTool(toolCall)) return 'toon_tool';
73
-
74
69
  // 检查已知工具名称
75
70
  for (const [pattern, type] of Object.entries(TOOL_TYPES)) {
76
71
  if (funcName.toLowerCase().includes(pattern)) return type;
@@ -84,19 +79,6 @@ class SampleClassifier {
84
79
  return 'unknown_tool';
85
80
  }
86
81
 
87
- // 检查是否为 TOON 工具
88
- isToonTool(toolCall) {
89
- const args = toolCall.function?.arguments;
90
- if (!args) return false;
91
-
92
- try {
93
- const parsed = JSON.parse(args);
94
- return parsed && typeof parsed.toon === 'string';
95
- } catch {
96
- return false;
97
- }
98
- }
99
-
100
82
  // 分析单个样本
101
83
  async analyzeSample(filePath) {
102
84
  try {
@@ -107,7 +89,6 @@ class SampleClassifier {
107
89
  const sampleId = basename(dirname(filePath)) + '/' + basename(filePath, '.json');
108
90
 
109
91
  let toolCalls = [];
110
- let hasToon = false;
111
92
 
112
93
  // 提取 tool_calls
113
94
  if (data.tool_calls) {
@@ -128,8 +109,6 @@ class SampleClassifier {
128
109
  const toolType = this.identifyToolType(toolCall);
129
110
  toolTypes.push(toolType);
130
111
 
131
- if (toolType === 'toon_tool') hasToon = true;
132
-
133
112
  // 更新统计
134
113
  this.stats.byToolType[toolType] = (this.stats.byToolType[toolType] || 0) + 1;
135
114
  }
@@ -140,7 +119,6 @@ class SampleClassifier {
140
119
  filePath,
141
120
  hasToolCalls: toolCalls.length > 0,
142
121
  toolTypes,
143
- hasToon,
144
122
  toolCallCount: toolCalls.length
145
123
  };
146
124
 
@@ -150,7 +128,6 @@ class SampleClassifier {
150
128
  this.stats.total++;
151
129
  this.stats.byProvider[providerKey] = (this.stats.byProvider[providerKey] || 0) + 1;
152
130
  if (toolCalls.length > 0) this.stats.withToolCalls++;
153
- if (hasToon) this.stats.withToon++;
154
131
 
155
132
  } catch (error) {
156
133
  console.error(`Error analyzing ${filePath}:`, error.message);
@@ -196,7 +173,6 @@ class SampleClassifier {
196
173
 
197
174
  console.log(`总样本数: ${this.stats.total}`);
198
175
  console.log(`包含工具调用: ${this.stats.withToolCalls}`);
199
- console.log(`包含 TOON: ${this.stats.withToon}`);
200
176
  console.log(`错误数: ${this.stats.errors}`);
201
177
 
202
178
  console.log('\n按 Provider 分布:');
@@ -224,21 +200,10 @@ class SampleClassifier {
224
200
  console.log('==================');
225
201
 
226
202
  const hasApplyPatch = this.stats.byToolType['apply_patch'] > 0;
227
- const hasToon = this.stats.byToolType['toon_tool'] > 0;
228
203
  const hasShell = this.stats.byToolType['shell_command'] > 0;
229
204
 
230
205
  if (!hasApplyPatch) console.log(' - 缺少 apply_patch 样本');
231
- if (!hasToon) console.log(' - 缺少 TOON 工具样本');
232
206
  if (!hasShell) console.log(' - 缺少 shell command 样本');
233
-
234
- // TOON 样本详情
235
- if (this.stats.withToon > 0) {
236
- console.log('\n🔧 TOON 工具样本:');
237
- const toonSamples = this.samples.filter(s => s.hasToon);
238
- for (const sample of toonSamples) {
239
- console.log(` - ${sample.provider}: ${sample.id}`);
240
- }
241
- }
242
207
  }
243
208
  }
244
209
 
@@ -6,6 +6,8 @@ async function copyModulesConfig() {
6
6
  const root = process.cwd();
7
7
  const srcModulesConfig = path.join(root, 'config', 'modules.json');
8
8
  const distModulesConfig = path.join(root, 'dist', 'config', 'modules.json');
9
+ const srcDaemonAdminUi = path.join(root, 'docs', 'daemon-admin-ui.html');
10
+ const distDaemonAdminUi = path.join(root, 'dist', 'docs', 'daemon-admin-ui.html');
9
11
 
10
12
  try {
11
13
  // 确保源文件存在
@@ -18,6 +20,20 @@ async function copyModulesConfig() {
18
20
  await fs.copyFile(srcModulesConfig, distModulesConfig);
19
21
 
20
22
  console.log('[copy-modules-config] copied modules.json to dist/config/modules.json');
23
+
24
+ try {
25
+ await fs.access(srcDaemonAdminUi);
26
+ await fs.mkdir(path.dirname(distDaemonAdminUi), { recursive: true });
27
+ await fs.copyFile(srcDaemonAdminUi, distDaemonAdminUi);
28
+ console.log('[copy-modules-config] copied daemon-admin-ui.html to dist/docs/daemon-admin-ui.html');
29
+ } catch (error) {
30
+ if (error.code === 'ENOENT') {
31
+ console.warn('[copy-modules-config] docs/daemon-admin-ui.html not found, skipping');
32
+ } else {
33
+ console.error('[copy-modules-config] failed to copy daemon admin ui:', error.message);
34
+ process.exit(1);
35
+ }
36
+ }
21
37
  } catch (error) {
22
38
  if (error.code === 'ENOENT') {
23
39
  console.warn('[copy-modules-config] source modules.json not found, skipping');
@@ -28,4 +44,4 @@ async function copyModulesConfig() {
28
44
  }
29
45
  }
30
46
 
31
- copyModulesConfig();
47
+ copyModulesConfig();
@@ -148,21 +148,26 @@ class SnapshotDataGenerator {
148
148
 
149
149
  try {
150
150
  await fs.mkdir(outputDir, { recursive: true });
151
- const files = await fs.readdir(responsesDir);
152
-
153
- const groups = this.groupFilesByRequestId(files);
154
151
  let count = 0;
155
-
156
- for (const [requestId, groupFiles] of Object.entries(groups)) {
152
+
153
+ const entries = await fs.readdir(responsesDir, { withFileTypes: true });
154
+
155
+ // New layout: openai-responses/<requestId>/*.json
156
+ for (const entry of entries) {
157
157
  if (count >= MAX_SAMPLES) break;
158
-
158
+ if (!entry.isDirectory()) continue;
159
+ const requestId = entry.name;
160
+ if (!requestId.startsWith('req_') && !requestId.includes('responses')) continue;
161
+
162
+ const subdirPath = path.join(responsesDir, requestId);
163
+ const files = (await fs.readdir(subdirPath)).filter((f) => f.endsWith('.json'));
159
164
  const snapshot = await this.buildSnapshotFromFiles(
160
- requestId,
161
- 'openai-responses',
162
- groupFiles,
163
- responsesDir
165
+ requestId,
166
+ 'openai-responses',
167
+ files,
168
+ subdirPath
164
169
  );
165
-
170
+
166
171
  if (snapshot) {
167
172
  const outputPath = path.join(outputDir, `${requestId}.json`);
168
173
  await fs.writeFile(outputPath, JSON.stringify(snapshot, null, 2));
@@ -171,6 +176,31 @@ class SnapshotDataGenerator {
171
176
  this.samplesGenerated++;
172
177
  }
173
178
  }
179
+
180
+ // Legacy layout: openai-responses/*.json
181
+ if (count < MAX_SAMPLES) {
182
+ const files = entries.filter((e) => e.isFile()).map((e) => e.name);
183
+ const groups = this.groupFilesByRequestId(files);
184
+
185
+ for (const [requestId, groupFiles] of Object.entries(groups)) {
186
+ if (count >= MAX_SAMPLES) break;
187
+
188
+ const snapshot = await this.buildSnapshotFromFiles(
189
+ requestId,
190
+ 'openai-responses',
191
+ groupFiles,
192
+ responsesDir
193
+ );
194
+
195
+ if (snapshot) {
196
+ const outputPath = path.join(outputDir, `${requestId}.json`);
197
+ await fs.writeFile(outputPath, JSON.stringify(snapshot, null, 2));
198
+ console.log(` ✅ 生成快照: ${requestId}`);
199
+ count++;
200
+ this.samplesGenerated++;
201
+ }
202
+ }
203
+ }
174
204
 
175
205
  console.log(` 📊 Responses: ${count} 个快照`);
176
206
  } catch (error) {
@@ -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);
@@ -198,6 +274,66 @@ async function extractPair(entryName, entryDir, file, registry, seqMap, filters
198
274
  console.log(`✅ ${reqId}`);
199
275
  }
200
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
+
201
337
  async function extractAll(options = {}) {
202
338
  console.log('[mock:extract] Preparing directories...');
203
339
  await ensureDir(MOCK_SAMPLES_DIR);
@@ -214,7 +350,41 @@ async function extractAll(options = {}) {
214
350
  continue;
215
351
  }
216
352
  const entryDir = path.join(CODEX_SAMPLES_DIR, dirEntry.name);
217
- 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);
218
388
  const requests = files.filter((name) => name.endsWith('_provider-request.json'));
219
389
  for (const file of requests) {
220
390
  try {
@@ -230,25 +400,73 @@ async function extractAll(options = {}) {
230
400
  }
231
401
 
232
402
  async function extractSingle(reqId) {
233
- const parts = reqId.split('-');
234
- if (parts.length < 5) {
235
- 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
420
+ }
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
236
441
  }
237
- const entry = parts.slice(0, parts.length - 4).join('-') || parts[0];
238
- const entryDir = path.join(CODEX_SAMPLES_DIR, entry);
442
+
239
443
  const files = await fs.readdir(entryDir);
240
444
  const target = files.find((file) => file.includes(reqId) && file.endsWith('_provider-request.json'));
241
445
  if (!target) {
242
- throw new Error(`Request ${reqId} not found under ${entry}`);
446
+ throw new Error(`Request ${reqId} not found under ${inferredEntry}`);
243
447
  }
244
- await ensureDir(MOCK_SAMPLES_DIR);
245
- await ensureDir(path.join(MOCK_SAMPLES_DIR, '_registry'));
246
- const registry = await loadRegistry();
247
- const seqMap = buildSeqMapFromRegistry(registry);
248
- await extractPair(entry, entryDir, target, registry, seqMap);
448
+ await extractPair(inferredEntry, entryDir, target, registry, seqMap);
249
449
  await saveRegistry(registry);
250
450
  }
251
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
+
252
470
  function parseArguments(argv) {
253
471
  const opts = { mode: 'all', reqId: undefined, provider: undefined, entry: undefined };
254
472
  for (const arg of argv) {