@jsonstudio/rcc 0.89.942 → 0.89.1086
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/README.md +1 -42
- package/dist/build-info.js +2 -2
- package/dist/build-info.js.map +1 -1
- package/dist/cli.js +181 -55
- package/dist/cli.js.map +1 -1
- package/dist/commands/quota-daemon.d.ts +2 -0
- package/dist/commands/quota-daemon.js +89 -0
- package/dist/commands/quota-daemon.js.map +1 -0
- package/dist/docs/daemon-admin-ui.html +958 -0
- package/dist/index.js +46 -10
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/quota/index.d.ts +34 -0
- package/dist/manager/modules/quota/index.js +291 -0
- package/dist/manager/modules/quota/index.js.map +1 -1
- package/dist/manager/modules/token/index.js +13 -2
- package/dist/manager/modules/token/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +48 -0
- package/dist/manager/quota/provider-quota-center.js +239 -0
- package/dist/manager/quota/provider-quota-center.js.map +1 -0
- package/dist/manager/quota/provider-quota-store.d.ts +17 -0
- package/dist/manager/quota/provider-quota-store.js +88 -0
- package/dist/manager/quota/provider-quota-store.js.map +1 -0
- package/dist/providers/auth/token-scanner/index.js +11 -3
- package/dist/providers/auth/token-scanner/index.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +24 -7
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.js +11 -3
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/responses-provider.js +9 -3
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/utils/http-client.d.ts +1 -0
- package/dist/providers/core/utils/http-client.js +139 -4
- package/dist/providers/core/utils/http-client.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
- package/dist/providers/core/utils/snapshot-writer.js +99 -18
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
- package/dist/providers/mock/mock-provider-runtime.js +176 -4
- package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
- package/dist/server/handlers/chat-handler.js +13 -1
- package/dist/server/handlers/chat-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.js +5 -0
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/messages-handler.js +13 -1
- package/dist/server/handlers/messages-handler.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +73 -1
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +6 -0
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +5 -0
- package/dist/server/runtime/http-server/index.js +205 -4
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.d.ts +2 -0
- package/dist/server/runtime/http-server/middleware.js +63 -0
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
- package/dist/server/runtime/http-server/request-executor.js +57 -10
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.js +38 -1
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
- package/dist/server/runtime/http-server/stats-manager.js +462 -4
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/types.d.ts +1 -0
- package/dist/token-daemon/token-daemon.js +70 -25
- package/dist/token-daemon/token-daemon.js.map +1 -1
- package/dist/token-daemon/token-utils.d.ts +1 -0
- package/dist/token-daemon/token-utils.js +9 -1
- package/dist/token-daemon/token-utils.js.map +1 -1
- package/dist/tools/semantic-replay.js +29 -0
- package/dist/tools/semantic-replay.js.map +1 -1
- package/dist/utils/is-direct-execution.d.ts +1 -0
- package/dist/utils/is-direct-execution.js +15 -0
- package/dist/utils/is-direct-execution.js.map +1 -0
- package/dist/utils/snapshot-writer.d.ts +2 -0
- package/dist/utils/snapshot-writer.js +47 -4
- package/dist/utils/snapshot-writer.js.map +1 -1
- package/dist/utils/windows-netstat.d.ts +1 -0
- package/dist/utils/windows-netstat.js +34 -0
- package/dist/utils/windows-netstat.js.map +1 -0
- package/package.json +3 -4
- package/scripts/analyze-codex-error-failures.mjs +24 -14
- package/scripts/classify-codex-samples.mjs +0 -35
- package/scripts/copy-modules-config.mjs +17 -1
- package/scripts/generate-snapshot-data.mjs +41 -11
- package/scripts/mock-provider/extract.mjs +239 -21
- package/scripts/mock-provider/run-regressions.mjs +79 -16
- package/scripts/quota-dryrun.mjs +124 -0
- package/scripts/tests/apply-patch-loop.mjs +5 -1
- package/scripts/tests/exec-command-loop.mjs +16 -19
- package/scripts/verify-apply-patch.mjs +335 -5
- package/scripts/verify-e2e-toolcall.mjs +49 -10
- package/scripts/toon-suite.mjs +0 -141
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsonstudio/rcc",
|
|
3
|
-
"version": "0.89.
|
|
3
|
+
"version": "0.89.1086",
|
|
4
4
|
"description": "Multi-provider OpenAI proxy server with anthropic/responses/chat support (release)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dev": "tsx watch src/index.ts",
|
|
38
38
|
"jest:run": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
|
39
39
|
"test": "npm run test:routing-instructions && npm run mock:regressions",
|
|
40
|
-
"test:routing-instructions": "npm run jest:run -- --runTestsByPath tests/server/runtime/request-executor.single-attempt.spec.ts tests/servertool/virtual-router-series-cooldown.spec.ts",
|
|
40
|
+
"test:routing-instructions": "npm run jest:run -- --runTestsByPath tests/server/runtime/request-executor.single-attempt.spec.ts tests/servertool/virtual-router-series-cooldown.spec.ts tests/utils/is-direct-execution.test.ts tests/utils/windows-netstat.test.ts",
|
|
41
41
|
"test:watch": "npm run jest:run -- --watch",
|
|
42
42
|
"test:coverage": "npm run jest:run -- --coverage",
|
|
43
43
|
"test:integration": "npm run jest:run -- --testPathPattern=integration",
|
|
@@ -72,7 +72,6 @@
|
|
|
72
72
|
"build:smoke": "npm run build && node scripts/protocol-smoke.mjs",
|
|
73
73
|
"verify:sse-loop": "node scripts/verify-sse-loop.mjs",
|
|
74
74
|
"snapshot:inspect": "node scripts/snapshot-inspect.mjs",
|
|
75
|
-
"probe:toon": "node scripts/toon-suite.mjs",
|
|
76
75
|
"debug:responses": "tsx tools/responses-debug-client/src/index.ts",
|
|
77
76
|
"debug:responses:lmstudio:text": "tsx tools/responses-debug-client/src/index.ts --file tools/responses-debug-client/payloads/lmstudio-text.json --baseURL ${LMSTUDIO_BASEURL:-http://127.0.0.1:1234/v1}",
|
|
78
77
|
"debug:responses:lmstudio:tool": "tsx tools/responses-debug-client/src/index.ts --file tools/responses-debug-client/payloads/lmstudio-tool.json --baseURL ${LMSTUDIO_BASEURL:-http://127.0.0.1:1234/v1}",
|
|
@@ -130,7 +129,7 @@
|
|
|
130
129
|
},
|
|
131
130
|
"dependencies": {
|
|
132
131
|
"@anthropic-ai/sdk": "^0.65.0",
|
|
133
|
-
"@jsonstudio/llms": "^0.6.
|
|
132
|
+
"@jsonstudio/llms": "^0.6.753",
|
|
134
133
|
"@jsonstudio/rcc": "^0.89.555",
|
|
135
134
|
"@lmstudio/sdk": "^1.5.0",
|
|
136
135
|
"@radix-ui/react-switch": "^1.2.6",
|
|
@@ -37,11 +37,25 @@ async function fileExists(p) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async function
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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?.
|
|
123
|
-
request?.
|
|
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
|
-
|
|
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
|
|
156
|
-
const tsToken = timestampToToken(request
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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 ${
|
|
446
|
+
throw new Error(`Request ${reqId} not found under ${inferredEntry}`);
|
|
243
447
|
}
|
|
244
|
-
await
|
|
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) {
|