@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.
- package/README.md +1 -42
- package/dist/build-info.js +2 -2
- package/dist/build-info.js.map +1 -1
- package/dist/cli.js +106 -10
- 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 +5 -1
- 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/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-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
|
@@ -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) {
|
|
@@ -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
|
+
|
|
@@ -8,6 +8,7 @@ import { Readable } from 'node:stream';
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import chalk from 'chalk';
|
|
11
12
|
import { createTempConfig, startServer, stopServer } from '../lib/routecodex-runner.mjs';
|
|
12
13
|
import { GeminiSemanticMapper } from '../../sharedmodule/llmswitch-core/dist/conversion/hub/semantic-mappers/gemini-mapper.js';
|
|
13
14
|
|
|
@@ -30,6 +31,8 @@ const STAGE_SUFFIX = '_req_outbound_stage2_format_build.json';
|
|
|
30
31
|
const STAGE1_SUFFIX = '_req_outbound_stage1_semantic_map.json';
|
|
31
32
|
const MOCK_PROVIDER_ID = 'mock.apply_patch.toolloop';
|
|
32
33
|
|
|
34
|
+
const chalkError = typeof chalk?.redBright === 'function' ? chalk.redBright : (value) => value;
|
|
35
|
+
|
|
33
36
|
function listProcessesOnPort(port) {
|
|
34
37
|
try {
|
|
35
38
|
const res = spawnSync('lsof', ['-ti', `tcp:${port}`], { encoding: 'utf-8' });
|
|
@@ -702,6 +705,7 @@ async function main() {
|
|
|
702
705
|
}
|
|
703
706
|
|
|
704
707
|
main().catch((error) => {
|
|
705
|
-
|
|
708
|
+
const msg = error instanceof Error ? (error.stack || error.message) : String(error ?? '');
|
|
709
|
+
console.error(chalkError(`[tool-loop] FAILED: ${msg}`));
|
|
706
710
|
process.exit(1);
|
|
707
711
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* exec_command
|
|
3
|
+
* exec_command JSON 形态回环验证(模拟 Responses 客户端)。
|
|
4
4
|
*
|
|
5
5
|
* 目标:
|
|
6
|
-
* - 构造一条带 exec_command
|
|
7
|
-
* - 通过 llmswitch-core 的 response
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
6
|
+
* - 构造一条带 exec_command JSON arguments 的 chat 响应;
|
|
7
|
+
* - 通过 llmswitch-core 的 response 工具过滤管线做统一治理;
|
|
8
|
+
* - 校验最终 JSON 形状(必须包含 cmd,且不暴露 toon);
|
|
9
|
+
* - 再通过 Responses 映射验证 /v1/responses 视图同样保持 JSON 语义。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import path from 'node:path';
|
|
@@ -25,9 +25,9 @@ async function main() {
|
|
|
25
25
|
'conversion/responses/responses-openai-bridge'
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
-
// 构造一条模拟的 chat 响应,其中 exec_command
|
|
28
|
+
// 构造一条模拟的 chat 响应,其中 exec_command 直接使用 JSON 编码参数。
|
|
29
29
|
const chatPayload = {
|
|
30
|
-
id: '
|
|
30
|
+
id: 'chatcmpl_exec_args',
|
|
31
31
|
object: 'chat.completion',
|
|
32
32
|
created: Math.floor(Date.now() / 1000),
|
|
33
33
|
model: 'gpt-5.2-codex',
|
|
@@ -39,18 +39,15 @@ async function main() {
|
|
|
39
39
|
content: null,
|
|
40
40
|
tool_calls: [
|
|
41
41
|
{
|
|
42
|
-
id: '
|
|
42
|
+
id: 'call_exec_args',
|
|
43
43
|
type: 'function',
|
|
44
44
|
function: {
|
|
45
45
|
name: 'exec_command',
|
|
46
46
|
arguments: JSON.stringify({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'shell: /bin/bash',
|
|
52
|
-
'login: false'
|
|
53
|
-
].join('\n')
|
|
47
|
+
cmd: 'echo 1',
|
|
48
|
+
workdir: '.',
|
|
49
|
+
yield_time_ms: 500,
|
|
50
|
+
max_output_tokens: 128
|
|
54
51
|
})
|
|
55
52
|
}
|
|
56
53
|
}
|
|
@@ -61,10 +58,10 @@ async function main() {
|
|
|
61
58
|
]
|
|
62
59
|
};
|
|
63
60
|
|
|
64
|
-
// 通过 response
|
|
61
|
+
// 通过 response 工具管线运行,触发统一的工具参数治理/归一化。
|
|
65
62
|
const filtered = await runChatResponseToolFilters(chatPayload, {
|
|
66
63
|
entryEndpoint: '/v1/chat/completions',
|
|
67
|
-
requestId: '
|
|
64
|
+
requestId: 'req_exec_args',
|
|
68
65
|
profile: 'openai-chat'
|
|
69
66
|
});
|
|
70
67
|
|
|
@@ -116,7 +113,7 @@ async function main() {
|
|
|
116
113
|
// 延伸验证:基于 chat 结果构建 Responses payload,确保 /v1/responses 视图中的
|
|
117
114
|
// function_call.arguments 同样保持 exec_command JSON 语义,而不会重新出现 toon。
|
|
118
115
|
const responsesPayload = buildResponsesPayloadFromChat(filtered, {
|
|
119
|
-
requestId: '
|
|
116
|
+
requestId: 'verify_exec_command_args'
|
|
120
117
|
});
|
|
121
118
|
const outputItems = Array.isArray(responsesPayload?.output) ? responsesPayload.output : [];
|
|
122
119
|
const fnCall = outputItems.find(
|
|
@@ -153,7 +150,7 @@ async function main() {
|
|
|
153
150
|
console.log(
|
|
154
151
|
`[exec-command-loop] decoded cmd="${args.cmd}" yield_time_ms=${args.yield_time_ms ?? 'n/a'} max_output_tokens=${args.max_output_tokens ?? 'n/a'}`
|
|
155
152
|
);
|
|
156
|
-
console.log('✅ exec_command
|
|
153
|
+
console.log('✅ exec_command arguments normalization passed (chat + responses views are JSON-only)');
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
main().catch((error) => {
|