@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.
- package/README.md +1 -42
- package/dist/build-info.js +2 -2
- package/dist/build-info.js.map +1 -1
- package/dist/cli.js +120 -16
- 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/commands/token-daemon.js +1 -1
- package/dist/commands/token-daemon.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +958 -0
- package/dist/index.js +54 -4
- 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 +14 -3
- 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.d.ts +3 -1
- package/dist/token-daemon/token-daemon.js +130 -8
- 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/snapshot-writer.d.ts +2 -0
- package/dist/utils/snapshot-writer.js +47 -4
- package/dist/utils/snapshot-writer.js.map +1 -1
- package/package.json +2 -3
- package/scripts/analyze-apply-patch-exec-failures.mjs +153 -0
- package/scripts/analyze-apply-patch-samples.mjs +242 -0
- 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 +254 -21
- package/scripts/mock-provider/run-regressions.mjs +97 -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
|
@@ -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);
|
|
@@ -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
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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 ${
|
|
446
|
+
throw new Error(`Request ${reqId} not found under ${inferredEntry}`);
|
|
228
447
|
}
|
|
229
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
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 (
|
|
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'
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|