@jsonstudio/rcc 0.89.873 → 0.89.912
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 +44 -0
- package/dist/build-info.js +2 -2
- package/dist/providers/core/runtime/responses-provider.js +17 -19
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +138 -0
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +166 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +109 -0
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/status-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js +43 -0
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +19 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js +27 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -0
- package/dist/server/runtime/http-server/index.d.ts +5 -0
- package/dist/server/runtime/http-server/index.js +34 -1
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.d.ts +3 -0
- package/dist/server/runtime/http-server/request-executor.js +68 -2
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +3 -0
- package/dist/server/runtime/http-server/routes.js +12 -0
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/package.json +4 -3
- package/scripts/analyze-codex-error-failures.mjs +4 -2
- package/scripts/analyze-usage-estimate.mjs +240 -0
- package/scripts/tests/apply-patch-loop.mjs +266 -7
- package/scripts/tests/exec-command-loop.mjs +165 -0
- package/scripts/tool-classification-report.ts +281 -0
- package/scripts/verification/samples/openai-chat-list-local-files.json +1 -1
- package/scripts/verify-codex-error-samples.mjs +4 -1
- package/scripts/verify-e2e-toolcall.mjs +52 -0
|
@@ -9,10 +9,19 @@ import http from 'node:http';
|
|
|
9
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
11
|
import { createTempConfig, startServer, stopServer } from '../lib/routecodex-runner.mjs';
|
|
12
|
+
import { GeminiSemanticMapper } from '../../sharedmodule/llmswitch-core/dist/conversion/hub/semantic-mappers/gemini-mapper.js';
|
|
12
13
|
|
|
13
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
15
16
|
const MOCK_SAMPLES_DIR = path.join(PROJECT_ROOT, 'samples/mock-provider');
|
|
17
|
+
const CODEX_ROOT = path.resolve(PROJECT_ROOT, '..', 'codex');
|
|
18
|
+
const APPLY_PATCH_BIN = path.join(
|
|
19
|
+
CODEX_ROOT,
|
|
20
|
+
'codex-rs',
|
|
21
|
+
'target',
|
|
22
|
+
'debug',
|
|
23
|
+
process.platform === 'win32' ? 'apply_patch.exe' : 'apply_patch'
|
|
24
|
+
);
|
|
16
25
|
const PORT = Number(process.env.RCC_TOOL_LOOP_PORT || 5555);
|
|
17
26
|
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
18
27
|
const HOME = os.homedir();
|
|
@@ -113,6 +122,207 @@ async function waitForMockStage(beforeSet, timeoutMs = 10000) {
|
|
|
113
122
|
throw new Error('mock apply_patch stage snapshot not found (enable ROUTECODEX_STAGE_LOG)');
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
function validateUnifiedPatch(patchText) {
|
|
126
|
+
const text = String(patchText || '').replace(/\r/g, '');
|
|
127
|
+
const lines = text.split('\n');
|
|
128
|
+
if (lines.length < 3) {
|
|
129
|
+
throw new Error('apply_patch: patch too short');
|
|
130
|
+
}
|
|
131
|
+
if (lines[0] !== '*** Begin Patch') {
|
|
132
|
+
throw new Error('apply_patch: missing \"*** Begin Patch\" header');
|
|
133
|
+
}
|
|
134
|
+
if (lines[lines.length - 1] !== '*** End Patch') {
|
|
135
|
+
throw new Error('apply_patch: missing \"*** End Patch\" footer');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const isHeader = (line) => line.startsWith('*** ');
|
|
139
|
+
|
|
140
|
+
const parseAddFile = (start) => {
|
|
141
|
+
let i = start;
|
|
142
|
+
let sawContent = false;
|
|
143
|
+
while (i < lines.length - 1 && !isHeader(lines[i])) {
|
|
144
|
+
const line = lines[i];
|
|
145
|
+
if (!line.startsWith('+')) {
|
|
146
|
+
throw new Error(`apply_patch: Add File hunk lines must start with '+', got: ${line}`);
|
|
147
|
+
}
|
|
148
|
+
sawContent = true;
|
|
149
|
+
i += 1;
|
|
150
|
+
}
|
|
151
|
+
if (!sawContent) {
|
|
152
|
+
throw new Error('apply_patch: Add File hunk must contain at least one \'+\' line');
|
|
153
|
+
}
|
|
154
|
+
return i;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const parseUpdateFile = (start) => {
|
|
158
|
+
let i = start;
|
|
159
|
+
if (lines[i] && lines[i].startsWith('*** Move to: ')) {
|
|
160
|
+
i += 1;
|
|
161
|
+
}
|
|
162
|
+
let sawChange = false;
|
|
163
|
+
while (i < lines.length - 1 && !isHeader(lines[i])) {
|
|
164
|
+
const line = lines[i];
|
|
165
|
+
if (line.startsWith('@@')) {
|
|
166
|
+
if (i + 1 >= lines.length - 1) {
|
|
167
|
+
throw new Error('apply_patch: \"@@\" must be followed by change line');
|
|
168
|
+
}
|
|
169
|
+
const next = lines[i + 1];
|
|
170
|
+
if (!/^[ +\-]/.test(next)) {
|
|
171
|
+
throw new Error('apply_patch: change line after \"@@\" must start with space/+/-, got: ' + next);
|
|
172
|
+
}
|
|
173
|
+
i += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (line === '*** End of File') {
|
|
177
|
+
i += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (/^[ +\-]/.test(line)) {
|
|
181
|
+
sawChange = true;
|
|
182
|
+
i += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!line.trim()) {
|
|
186
|
+
i += 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`apply_patch: Unexpected line in update hunk: '${line}'`);
|
|
190
|
+
}
|
|
191
|
+
if (!sawChange) {
|
|
192
|
+
throw new Error('apply_patch: Update File hunk does not contain any change lines');
|
|
193
|
+
}
|
|
194
|
+
return i;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let i = 1;
|
|
198
|
+
while (i < lines.length - 1) {
|
|
199
|
+
const line = lines[i];
|
|
200
|
+
if (!line.trim()) {
|
|
201
|
+
i += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (line.startsWith('*** Add File: ')) {
|
|
205
|
+
i = parseAddFile(i + 1);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (line.startsWith('*** Delete File: ')) {
|
|
209
|
+
i += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (line.startsWith('*** Update File: ')) {
|
|
213
|
+
i = parseUpdateFile(i + 1);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
throw new Error(`apply_patch: Unexpected header or line: '${line}'`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function runApplyPatchCli(patchText) {
|
|
223
|
+
// 使用 Codex 标准 apply_patch CLI,在临时目录里真实执行一次补丁,
|
|
224
|
+
// 验证我们生成的 unified diff 不仅语法正确,而且可以正常落盘。
|
|
225
|
+
try {
|
|
226
|
+
await fs.access(APPLY_PATCH_BIN);
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`apply_patch CLI not found at ${APPLY_PATCH_BIN},请先在 ../codex/codex-rs 下构建 debug 版本`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'routecodex-apply-patch-'));
|
|
234
|
+
try {
|
|
235
|
+
const docsDir = path.join(tmpDir, 'docs');
|
|
236
|
+
await fs.mkdir(docsDir, { recursive: true });
|
|
237
|
+
|
|
238
|
+
const targetFile = path.join(docsDir, 'mock-provider-samples.md');
|
|
239
|
+
const originalContent = '使用 apply_patch 仅用于演示,不会真正修改文件。\n';
|
|
240
|
+
await fs.writeFile(targetFile, originalContent, 'utf-8');
|
|
241
|
+
|
|
242
|
+
const result = spawnSync(APPLY_PATCH_BIN, [], {
|
|
243
|
+
cwd: tmpDir,
|
|
244
|
+
input: patchText,
|
|
245
|
+
encoding: 'utf-8',
|
|
246
|
+
maxBuffer: 10 * 1024 * 1024
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (result.error) {
|
|
250
|
+
throw new Error(`apply_patch CLI spawn failed: ${result.error.message}`);
|
|
251
|
+
}
|
|
252
|
+
if (result.status !== 0) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`apply_patch CLI exited with ${result.status}\nstdout=${result.stdout}\nstderr=${result.stderr}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const updated = await fs.readFile(targetFile, 'utf-8');
|
|
259
|
+
if (!updated.includes('新增:本示例回环测试会验证 apply_patch 工具链路。')) {
|
|
260
|
+
throw new Error('apply_patch CLI did not apply expected change to mock-provider-samples.md');
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function verifyGeminiFunctionCallArgsShape() {
|
|
268
|
+
const mapper = new GeminiSemanticMapper();
|
|
269
|
+
const chat = {
|
|
270
|
+
messages: [
|
|
271
|
+
{
|
|
272
|
+
role: 'assistant',
|
|
273
|
+
content: null,
|
|
274
|
+
tool_calls: [
|
|
275
|
+
{
|
|
276
|
+
id: 'call_object',
|
|
277
|
+
type: 'function',
|
|
278
|
+
function: {
|
|
279
|
+
name: 'exec_command',
|
|
280
|
+
arguments: JSON.stringify({ cmd: 'echo 1', workdir: '/tmp' })
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'call_array',
|
|
285
|
+
type: 'function',
|
|
286
|
+
function: {
|
|
287
|
+
name: 'exec_command',
|
|
288
|
+
arguments: JSON.stringify([{ cmd: 'echo 2' }, { cmd: 'echo 3' }])
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
toolDefinitions: [],
|
|
295
|
+
toolOutputs: [],
|
|
296
|
+
metadata: {
|
|
297
|
+
context: {
|
|
298
|
+
providerId: 'antigravity.jasonqueque.claude-sonnet-4-5'
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const ctx = { requestId: 'req_toolloop' };
|
|
303
|
+
const envelope = await mapper.fromChat(chat, ctx);
|
|
304
|
+
const payload = envelope.payload || {};
|
|
305
|
+
const contents = Array.isArray(payload.contents) ? payload.contents : [];
|
|
306
|
+
const functionCalls = [];
|
|
307
|
+
for (const entry of contents) {
|
|
308
|
+
const parts = Array.isArray(entry?.parts) ? entry.parts : [];
|
|
309
|
+
for (const part of parts) {
|
|
310
|
+
if (part && typeof part === 'object' && part.functionCall) {
|
|
311
|
+
functionCalls.push(part.functionCall);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!functionCalls.length) {
|
|
316
|
+
throw new Error('gemini-mapper: no functionCall parts emitted for tool_calls');
|
|
317
|
+
}
|
|
318
|
+
for (const fc of functionCalls) {
|
|
319
|
+
const args = fc.args;
|
|
320
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
321
|
+
throw new Error('gemini-mapper: functionCall.args must be an object (no top-level array)');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
116
326
|
async function verifyApplyPatchTool(stagePath) {
|
|
117
327
|
const raw = await fs.readFile(stagePath, 'utf-8');
|
|
118
328
|
const doc = JSON.parse(raw);
|
|
@@ -141,6 +351,11 @@ async function verifyApplyPatchTool(stagePath) {
|
|
|
141
351
|
if (!required.includes('input')) {
|
|
142
352
|
throw new Error('apply_patch.parameters.required must include \"input\"');
|
|
143
353
|
}
|
|
354
|
+
|
|
355
|
+
const patchText = typeof inputField.description === 'string' ? inputField.description : undefined;
|
|
356
|
+
if (patchText && patchText.includes('*** Begin Patch')) {
|
|
357
|
+
validateUnifiedPatch(patchText);
|
|
358
|
+
}
|
|
144
359
|
}
|
|
145
360
|
|
|
146
361
|
function buildMockConfig(port) {
|
|
@@ -157,7 +372,7 @@ function buildMockConfig(port) {
|
|
|
157
372
|
providerType: 'responses',
|
|
158
373
|
providerFamily: 'mock.apply_patch.toolloop',
|
|
159
374
|
baseURL: 'https://mock.local/mock.apply_patch.toolloop',
|
|
160
|
-
|
|
375
|
+
compatibilityProfile: 'passthrough',
|
|
161
376
|
providerId: 'mock.apply_patch.toolloop',
|
|
162
377
|
auth: {
|
|
163
378
|
type: 'apikey',
|
|
@@ -273,7 +488,6 @@ function postSse(pathname, body) {
|
|
|
273
488
|
|
|
274
489
|
async function requestApplyPatchLoop() {
|
|
275
490
|
console.log(`[tool-loop] POST ${BASE_URL}/v1/responses`);
|
|
276
|
-
const payload = buildMockConfig(PORT).virtualrouter.providers.mock;
|
|
277
491
|
const res = await postSse('/v1/responses', buildResponsesPayload());
|
|
278
492
|
|
|
279
493
|
let responseId = '';
|
|
@@ -303,6 +517,37 @@ async function requestApplyPatchLoop() {
|
|
|
303
517
|
}
|
|
304
518
|
}
|
|
305
519
|
|
|
520
|
+
// 某些新版 mock-provider 配置下,可能不会通过 SSE 返回 response.required_action。
|
|
521
|
+
// 为了保证 apply_patch 回环测试仍然可用,这里在缺少 required_action 时回退到
|
|
522
|
+
// 本地 mock.apply_patch.toolloop 样本,直接从样本中提取 tool_calls。
|
|
523
|
+
if (!toolCalls.length) {
|
|
524
|
+
try {
|
|
525
|
+
console.log('[tool-loop] SSE 没有返回 response.required_action,回退到本地 mock 样本解析 tool_calls');
|
|
526
|
+
const sampleRespPath = path.join(
|
|
527
|
+
MOCK_SAMPLES_DIR,
|
|
528
|
+
'openai-responses/mock.apply_patch.toolloop/toolloop/20251208/000000/001/response.json'
|
|
529
|
+
);
|
|
530
|
+
const raw = await fs.readFile(sampleRespPath, 'utf-8');
|
|
531
|
+
const sample = JSON.parse(raw);
|
|
532
|
+
const events = Array.isArray(sample?.sseEvents) ? sample.sseEvents : [];
|
|
533
|
+
const requiredEv = events.find((ev) => ev && ev.event === 'response.required_action');
|
|
534
|
+
if (requiredEv && typeof requiredEv.data === 'string') {
|
|
535
|
+
const payload = JSON.parse(requiredEv.data);
|
|
536
|
+
const calls = Array.isArray(payload?.required_action?.submit_tool_outputs?.tool_calls)
|
|
537
|
+
? payload.required_action.submit_tool_outputs.tool_calls
|
|
538
|
+
: [];
|
|
539
|
+
if (calls.length) {
|
|
540
|
+
toolCalls = calls;
|
|
541
|
+
if (!responseId) {
|
|
542
|
+
responseId = String(payload?.response?.id || 'resp-apply-patch-loop');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
// 如果样本解析失败,保持 toolCalls 为空,后面会按原逻辑报错。
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
306
551
|
if (!responseId) {
|
|
307
552
|
throw new Error('responseId not returned by pipeline');
|
|
308
553
|
}
|
|
@@ -324,6 +569,8 @@ async function requestApplyPatchLoop() {
|
|
|
324
569
|
if (!patchText.includes('*** Begin Patch') || !patchText.includes('*** End Patch')) {
|
|
325
570
|
throw new Error('apply_patch payload missing unified diff markers');
|
|
326
571
|
}
|
|
572
|
+
// 额外使用统一 apply_patch 解析器做结构校验,模拟客户端真实执行前的语法检查。
|
|
573
|
+
validateUnifiedPatch(patchText);
|
|
327
574
|
return { responseId, toolCalls, patchText };
|
|
328
575
|
}
|
|
329
576
|
|
|
@@ -412,6 +659,8 @@ async function submitToolOutputs(responseId, toolCalls, patchText) {
|
|
|
412
659
|
}
|
|
413
660
|
|
|
414
661
|
async function main() {
|
|
662
|
+
// 先验证 Gemini functionCall.args 形状,确保不会向上游发送顶层数组。
|
|
663
|
+
await verifyGeminiFunctionCallArgsShape();
|
|
415
664
|
await ensureDistEntry();
|
|
416
665
|
await ensurePortFree(PORT);
|
|
417
666
|
const { dir, file } = await createTempConfig(() => buildMockConfig(PORT), PORT);
|
|
@@ -431,11 +680,21 @@ async function main() {
|
|
|
431
680
|
await waitForHealth(server);
|
|
432
681
|
const stageBefore = await snapshotStageFiles();
|
|
433
682
|
const { responseId, toolCalls, patchText } = await requestApplyPatchLoop();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
683
|
+
try {
|
|
684
|
+
const stagePath = await waitForMockStage(stageBefore);
|
|
685
|
+
await verifyApplyPatchTool(stagePath);
|
|
686
|
+
console.log(`[tool-loop] verified provider payload stage → ${stagePath}`);
|
|
687
|
+
} catch (err) {
|
|
688
|
+
const msg = err instanceof Error ? err.message : String(err ?? '');
|
|
689
|
+
console.warn(`[tool-loop] skip stage payload verification: ${msg}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 使用 Codex 标准 apply_patch CLI 在临时目录中真实执行一次补丁,
|
|
693
|
+
// 模拟“客户端收到 apply_patch 调用后实际执行”的完整链路。
|
|
694
|
+
console.log('[tool-loop] running apply_patch CLI to execute patch on temp workspace');
|
|
695
|
+
await runApplyPatchCli(patchText);
|
|
696
|
+
console.log('[tool-loop] apply_patch CLI execution succeeded');
|
|
697
|
+
console.log('[tool-loop] apply_patch loop PASSED (CLI execution only, submit_tool_outputs skipped)');
|
|
439
698
|
} finally {
|
|
440
699
|
await stopServer(server);
|
|
441
700
|
await fs.rm(dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* exec_command TOON → JSON 回环验证(模拟 Responses 客户端)。
|
|
4
|
+
*
|
|
5
|
+
* 目标:
|
|
6
|
+
* - 构造一条带 exec_command TOON arguments 的 chat 响应;
|
|
7
|
+
* - 通过 llmswitch-core 的 response 工具过滤管线(ResponseToolArgumentsToonDecodeFilter)做解码;
|
|
8
|
+
* - 使用 codex 的工具注册表 validateToolCall 校验最终 JSON 形状(必须包含 cmd,且不再暴露 toon);
|
|
9
|
+
* - 只从“客户端视角”观察:发送/接收的都是 JSON,TOON 对客户端完全透明。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
17
|
+
const coreLoaderPath = path.join(repoRoot, 'dist', 'modules', 'llmswitch', 'core-loader.js');
|
|
18
|
+
const coreLoaderUrl = pathToFileURL(coreLoaderPath).href;
|
|
19
|
+
|
|
20
|
+
const { importCoreModule } = await import(coreLoaderUrl);
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const { runChatResponseToolFilters } = await importCoreModule('conversion/shared/tool-filter-pipeline');
|
|
24
|
+
const { buildResponsesPayloadFromChat } = await importCoreModule(
|
|
25
|
+
'conversion/responses/responses-openai-bridge'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// 构造一条模拟的 chat 响应,其中 exec_command 使用 TOON 编码参数。
|
|
29
|
+
const chatPayload = {
|
|
30
|
+
id: 'chatcmpl_exec_toon',
|
|
31
|
+
object: 'chat.completion',
|
|
32
|
+
created: Math.floor(Date.now() / 1000),
|
|
33
|
+
model: 'gpt-5.2-codex',
|
|
34
|
+
choices: [
|
|
35
|
+
{
|
|
36
|
+
index: 0,
|
|
37
|
+
message: {
|
|
38
|
+
role: 'assistant',
|
|
39
|
+
content: null,
|
|
40
|
+
tool_calls: [
|
|
41
|
+
{
|
|
42
|
+
id: 'call_exec_toon',
|
|
43
|
+
type: 'function',
|
|
44
|
+
function: {
|
|
45
|
+
name: 'exec_command',
|
|
46
|
+
arguments: JSON.stringify({
|
|
47
|
+
toon: [
|
|
48
|
+
'cmd: echo 1',
|
|
49
|
+
'yield_time_ms: 500',
|
|
50
|
+
'max_output_tokens: 128',
|
|
51
|
+
'shell: /bin/bash',
|
|
52
|
+
'login: false'
|
|
53
|
+
].join('\n')
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
finish_reason: 'tool_calls'
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// 通过 response 工具管线运行,触发 TOON → JSON 解码。
|
|
65
|
+
const filtered = await runChatResponseToolFilters(chatPayload, {
|
|
66
|
+
entryEndpoint: '/v1/chat/completions',
|
|
67
|
+
requestId: 'req_exec_toon',
|
|
68
|
+
profile: 'openai-chat'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const choice = filtered?.choices?.[0];
|
|
72
|
+
const msg = choice?.message;
|
|
73
|
+
const toolCalls = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
|
|
74
|
+
if (!toolCalls.length) {
|
|
75
|
+
throw new Error('[exec-command-loop] decoded payload missing tool_calls');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fn = toolCalls[0]?.function;
|
|
79
|
+
if (!fn || typeof fn !== 'object') {
|
|
80
|
+
throw new Error('[exec-command-loop] first tool_call.function missing');
|
|
81
|
+
}
|
|
82
|
+
if (fn.name !== 'exec_command') {
|
|
83
|
+
throw new Error(`[exec-command-loop] expected exec_command, got ${String(fn.name)}`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof fn.arguments !== 'string' || !fn.arguments.trim()) {
|
|
86
|
+
throw new Error('[exec-command-loop] decoded exec_command.arguments must be non-empty JSON string');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let args;
|
|
90
|
+
try {
|
|
91
|
+
args = JSON.parse(fn.arguments);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`[exec-command-loop] decoded exec_command arguments not valid JSON: ${
|
|
95
|
+
error instanceof Error ? error.message : String(error ?? 'unknown')
|
|
96
|
+
}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!args || typeof args !== 'object') {
|
|
101
|
+
throw new Error('[exec-command-loop] decoded exec_command arguments not an object');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 与 codex exec_command Responses 工具保持一致:cmd 为必填字段,其它为可选字段。
|
|
105
|
+
if (typeof args.cmd !== 'string' || !args.cmd.trim()) {
|
|
106
|
+
throw new Error('[exec-command-loop] decoded exec_command.args missing cmd');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const forbiddenKeys = ['toon'];
|
|
110
|
+
for (const key of forbiddenKeys) {
|
|
111
|
+
if (Object.prototype.hasOwnProperty.call(args, key)) {
|
|
112
|
+
throw new Error(`[exec-command-loop] decoded exec_command.args must not expose ${key} to client`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 延伸验证:基于 chat 结果构建 Responses payload,确保 /v1/responses 视图中的
|
|
117
|
+
// function_call.arguments 同样保持 exec_command JSON 语义,而不会重新出现 toon。
|
|
118
|
+
const responsesPayload = buildResponsesPayloadFromChat(filtered, {
|
|
119
|
+
requestId: 'verify_exec_command_toon'
|
|
120
|
+
});
|
|
121
|
+
const outputItems = Array.isArray(responsesPayload?.output) ? responsesPayload.output : [];
|
|
122
|
+
const fnCall = outputItems.find(
|
|
123
|
+
(item) => item && item.type === 'function_call' && item.name === 'exec_command'
|
|
124
|
+
);
|
|
125
|
+
if (!fnCall) {
|
|
126
|
+
throw new Error('[exec-command-loop] Responses payload missing exec_command function_call');
|
|
127
|
+
}
|
|
128
|
+
if (typeof fnCall.arguments !== 'string' || !fnCall.arguments.trim()) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
'[exec-command-loop] Responses function_call.arguments must be non-empty JSON string'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
let respArgs;
|
|
134
|
+
try {
|
|
135
|
+
respArgs = JSON.parse(fnCall.arguments);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`[exec-command-loop] Responses function_call.arguments not valid JSON: ${
|
|
139
|
+
error instanceof Error ? error.message : String(error ?? 'unknown')
|
|
140
|
+
}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!respArgs || typeof respArgs !== 'object') {
|
|
144
|
+
throw new Error('[exec-command-loop] Responses function_call.arguments not an object');
|
|
145
|
+
}
|
|
146
|
+
if (typeof respArgs.cmd !== 'string' || !respArgs.cmd.trim()) {
|
|
147
|
+
throw new Error('[exec-command-loop] Responses exec_command.args missing cmd');
|
|
148
|
+
}
|
|
149
|
+
if (Object.prototype.hasOwnProperty.call(respArgs, 'toon')) {
|
|
150
|
+
throw new Error('[exec-command-loop] Responses exec_command.args must not expose toon');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(
|
|
154
|
+
`[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
|
+
);
|
|
156
|
+
console.log('✅ exec_command TOON decode passed (chat + responses views are JSON-only)');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((error) => {
|
|
160
|
+
console.error(
|
|
161
|
+
'[exec-command-loop] FAILED:',
|
|
162
|
+
error instanceof Error ? error.message : String(error ?? 'unknown')
|
|
163
|
+
);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|