codexmate 0.0.38 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/cli/builtin-proxy.js +626 -207
  2. package/cli/config-bootstrap.js +6 -1
  3. package/cli/openai-bridge.js +541 -210
  4. package/cli.js +189 -4
  5. package/package.json +1 -1
  6. package/plugins/prompt-templates/computed.mjs +61 -3
  7. package/plugins/prompt-templates/manifest.mjs +3 -0
  8. package/web-ui/app.js +14 -3
  9. package/web-ui/modules/app.computed.main-tabs.mjs +39 -30
  10. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  11. package/web-ui/modules/app.methods.index.mjs +2 -0
  12. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  13. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  14. package/web-ui/modules/app.methods.providers.mjs +36 -10
  15. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  16. package/web-ui/modules/app.methods.startup-claude.mjs +7 -0
  17. package/web-ui/modules/app.methods.tool-config-permissions.mjs +87 -0
  18. package/web-ui/modules/config-mode.computed.mjs +3 -3
  19. package/web-ui/modules/i18n/locales/en.mjs +1140 -0
  20. package/web-ui/modules/i18n/locales/ja.mjs +1130 -0
  21. package/web-ui/modules/i18n/locales/vi.mjs +239 -0
  22. package/web-ui/modules/i18n/locales/zh.mjs +1143 -0
  23. package/web-ui/modules/i18n.dict.mjs +9 -3195
  24. package/web-ui/modules/i18n.mjs +65 -16
  25. package/web-ui/partials/index/layout-header.html +16 -46
  26. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  27. package/web-ui/partials/index/modal-webhook.html +8 -8
  28. package/web-ui/partials/index/modals-basic.html +56 -16
  29. package/web-ui/partials/index/panel-config-claude.html +51 -21
  30. package/web-ui/partials/index/panel-config-codex.html +34 -5
  31. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  32. package/web-ui/partials/index/panel-dashboard.html +62 -77
  33. package/web-ui/partials/index/panel-settings.html +28 -7
  34. package/web-ui/partials/index/panel-trash.html +14 -14
  35. package/web-ui/res/web-ui-render.precompiled.js +1783 -1386
  36. package/web-ui/styles/controls-forms.css +99 -0
  37. package/web-ui/styles/dashboard.css +46 -14
  38. package/web-ui/styles/layout-shell.css +45 -0
  39. package/web-ui/styles/navigation-panels.css +3 -3
  40. package/web-ui/styles/openclaw-structured.css +383 -33
  41. package/web-ui/styles/responsive.css +68 -0
  42. package/web-ui/styles/sessions-usage.css +105 -9
  43. package/web-ui/styles/settings-panel.css +4 -0
@@ -222,226 +222,530 @@ function extractChatCompletionResult(payload) {
222
222
  return { text, toolCalls };
223
223
  }
224
224
 
225
- function normalizeResponsesInputToChatMessages(input) {
226
- // 支持:
227
- // - string
228
- // - { role, content }(单条 message)
229
- // - { type:"input_text"|"input_image", ... }(单个 block)
230
- // - [{ role, content: [...] }](messages array)
231
- // - [{ type:"input_text"|"input_image", ... }](blocks array -> 单条 user 消息)
232
- if (typeof input === 'string') {
233
- return [{ role: 'user', content: input }];
234
- }
235
-
236
- const toChatContent = (blocks) => {
237
- if (!Array.isArray(blocks)) return '';
238
- const out = [];
239
- for (const block of blocks) {
240
- if (!block || typeof block !== 'object') continue;
241
- const type = typeof block.type === 'string' ? block.type : '';
242
- if ((type === 'input_text' || type === 'output_text') && typeof block.text === 'string') {
243
- out.push({ type: 'text', text: block.text });
244
- continue;
245
- }
246
- if ((type === 'reasoning' || type === 'reasoning_text' || type === 'reasoning_content') && typeof block.text === 'string') {
247
- out.push({ type: 'text', text: block.text });
248
- continue;
249
- }
250
- if (type === 'input_image') {
251
- const raw = block.image_url != null ? block.image_url : block.imageUrl;
252
- const url = typeof raw === 'string'
253
- ? raw
254
- : (raw && typeof raw === 'object' && typeof raw.url === 'string' ? raw.url : '');
255
- if (url) {
256
- out.push({ type: 'image_url', image_url: { url } });
257
- }
258
- continue;
259
- }
260
- // 容错:兼容已是 chat content 的 {type:"text"} / {type:"image_url"}
261
- if (type === 'text' && typeof block.text === 'string') {
262
- out.push({ type: 'text', text: block.text });
263
- continue;
264
- }
265
- if (type === 'image_url' && block.image_url) {
266
- out.push({ type: 'image_url', image_url: block.image_url });
267
- continue;
268
- }
269
- const text = typeof block.text === 'string'
270
- ? block.text
271
- : (typeof block.content === 'string' ? block.content : '');
272
- if (text) {
273
- out.push({ type: 'text', text });
274
- continue;
275
- }
276
- try {
277
- const raw = JSON.stringify(block);
278
- if (raw) {
279
- out.push({ type: 'text', text: raw.slice(0, 4000) });
280
- }
281
- } catch (_) {}
225
+ function stringifyJsonValue(value, fallback = '') {
226
+ if (typeof value === 'string') return value;
227
+ if (value == null) return fallback;
228
+ try {
229
+ return JSON.stringify(value);
230
+ } catch (_) {
231
+ return fallback;
232
+ }
233
+ }
234
+
235
+ function parseJsonValueOrNull(value) {
236
+ if (typeof value !== 'string') return null;
237
+ const text = value.trim();
238
+ if (!text) return null;
239
+ try {
240
+ return JSON.parse(text);
241
+ } catch (_) {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ function isRecord(value) {
247
+ return !!value && typeof value === 'object' && !Array.isArray(value);
248
+ }
249
+
250
+ function asTrimmedString(value) {
251
+ return typeof value === 'string' ? value.trim() : '';
252
+ }
253
+
254
+ function cloneJsonValue(value) {
255
+ if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
256
+ if (isRecord(value)) {
257
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneJsonValue(item)]));
258
+ }
259
+ return value;
260
+ }
261
+
262
+ function normalizeResponsesToolOutput(value) {
263
+ if (typeof value === 'string') return value;
264
+ if (value == null) return '';
265
+ return stringifyJsonValue(value, '');
266
+ }
267
+
268
+ function normalizeOpenAiToolArguments(value) {
269
+ if (typeof value === 'string') return value;
270
+ if (value == null) return '{}';
271
+ return stringifyJsonValue(value, '{}');
272
+ }
273
+
274
+ function normalizeInputFileBlock(item) {
275
+ if (!isRecord(item)) return null;
276
+ const file = isRecord(item.file) ? item.file : item;
277
+ const out = {};
278
+ const fileId = asTrimmedString(file.file_id || file.id);
279
+ const filename = asTrimmedString(file.filename || file.name);
280
+ const fileData = asTrimmedString(file.file_data || file.data);
281
+ const mimeType = asTrimmedString(file.mime_type || file.media_type);
282
+ if (fileId) out.file_id = fileId;
283
+ if (filename) out.filename = filename;
284
+ if (fileData) out.file_data = fileData;
285
+ if (mimeType) out.mime_type = mimeType;
286
+ return Object.keys(out).length > 0 ? out : null;
287
+ }
288
+
289
+ function normalizeResponsesContentBlockForChat(item) {
290
+ if (typeof item === 'string') return item.trim() ? item : null;
291
+ if (!isRecord(item)) return null;
292
+
293
+ const type = asTrimmedString(item.type).toLowerCase();
294
+ if (!type) {
295
+ const text = asTrimmedString(item.text || item.content || item.output_text);
296
+ return text ? { type: 'text', text } : null;
297
+ }
298
+
299
+ if (type === 'input_text' || type === 'output_text' || type === 'text' || type === 'summary_text' || type === 'reasoning_text') {
300
+ const text = typeof item.text === 'string' ? item.text : asTrimmedString(item.content || item.output_text);
301
+ return text ? { type: 'text', text } : null;
302
+ }
303
+
304
+ if (type === 'refusal' && typeof item.refusal === 'string') {
305
+ return item.refusal ? { type: 'text', text: item.refusal } : null;
306
+ }
307
+
308
+ if (type === 'input_image') {
309
+ const raw = item.image_url != null ? item.image_url : (item.url != null ? item.url : item.imageUrl);
310
+ if (raw === undefined) return null;
311
+ return {
312
+ type: 'image_url',
313
+ image_url: typeof raw === 'string' ? { url: raw } : cloneJsonValue(raw)
314
+ };
315
+ }
316
+
317
+ if (type === 'image_url' && item.image_url !== undefined) {
318
+ return { type: 'image_url', image_url: item.image_url };
319
+ }
320
+
321
+ if (type === 'input_audio') {
322
+ if (item.input_audio !== undefined) return { type: 'input_audio', input_audio: item.input_audio };
323
+ if (item.data !== undefined || item.format !== undefined) {
324
+ return { type: 'input_audio', input_audio: { data: item.data, format: item.format } };
282
325
  }
283
- if (out.length === 0) return '';
284
- return out;
285
- };
326
+ return null;
327
+ }
286
328
 
287
- const toRole = (value) => {
288
- const roleRaw = typeof value === 'string' ? value.trim().toLowerCase() : '';
289
- if (roleRaw === 'assistant') return 'assistant';
290
- // codex 把 AGENTS.md 注入 developer 角色;Responses 的 developer 在 chat 侧等价于 system。
291
- if (roleRaw === 'system' || roleRaw === 'developer') return 'system';
292
- return 'user';
293
- };
329
+ if (type === 'input_file' || type === 'file') {
330
+ const file = normalizeInputFileBlock(item);
331
+ return file ? { type: 'file', file } : null;
332
+ }
333
+
334
+ if (type === 'reasoning' || type === 'thinking' || type === 'redacted_reasoning') {
335
+ const text = asTrimmedString(item.text || item.content);
336
+ return text ? { type: 'text', text } : null;
337
+ }
294
338
 
295
- if (input && typeof input === 'object' && !Array.isArray(input)) {
296
- if (typeof input.role === 'string' && input.content != null) {
297
- const role = toRole(input.role);
298
- const content = Array.isArray(input.content)
299
- ? toChatContent(input.content)
300
- : input.content;
301
- return content ? [{ role, content }] : [];
339
+ const text = asTrimmedString(item.text || item.content);
340
+ return text ? { type: 'text', text } : cloneJsonValue(item);
341
+ }
342
+
343
+ function toOpenAiMessageContent(content) {
344
+ if (typeof content === 'string') return content;
345
+ if (!Array.isArray(content)) {
346
+ if (isRecord(content)) {
347
+ const single = normalizeResponsesContentBlockForChat(content);
348
+ if (!single) return '';
349
+ return typeof single === 'string' ? single : [single];
302
350
  }
303
- if (typeof input.type === 'string') {
304
- const content = toChatContent([input]);
305
- return content ? [{ role: 'user', content }] : [];
351
+ return '';
352
+ }
353
+
354
+ const blocks = content
355
+ .map((item) => normalizeResponsesContentBlockForChat(item))
356
+ .filter((item) => !!item);
357
+
358
+ if (blocks.length === 0) return '';
359
+ if (blocks.length === 1 && typeof blocks[0] === 'string') return blocks[0];
360
+ return blocks;
361
+ }
362
+
363
+ const RESPONSES_TOOL_CALL_INPUT_TYPES = new Set(['function_call', 'custom_tool_call', 'mcp_tool_call', 'local_shell_call']);
364
+ const RESPONSES_TOOL_CALL_OUTPUT_TYPES = new Set(['function_call_output', 'custom_tool_call_output', 'mcp_tool_call_output', 'tool_search_output', 'local_shell_call_output']);
365
+
366
+ function stripOrphanedResponsesToolOutputs(input) {
367
+ if (!Array.isArray(input)) return input;
368
+ const seenToolCallIds = new Set();
369
+ const sanitized = [];
370
+ for (const item of input) {
371
+ if (!isRecord(item)) {
372
+ sanitized.push(item);
373
+ continue;
374
+ }
375
+ const type = asTrimmedString(item.type).toLowerCase();
376
+ if (RESPONSES_TOOL_CALL_INPUT_TYPES.has(type)) {
377
+ const callId = asTrimmedString(item.call_id || item.id);
378
+ if (callId) seenToolCallIds.add(callId);
379
+ sanitized.push(item);
380
+ continue;
381
+ }
382
+ if (RESPONSES_TOOL_CALL_OUTPUT_TYPES.has(type)) {
383
+ const callId = asTrimmedString(item.call_id || item.id);
384
+ if (!callId || !seenToolCallIds.has(callId)) continue;
385
+ sanitized.push(item);
386
+ continue;
306
387
  }
307
- return [];
388
+ sanitized.push(item);
308
389
  }
390
+ return sanitized;
391
+ }
309
392
 
310
- if (!Array.isArray(input)) {
311
- return [];
393
+ function normalizeFreeformToolArguments(value) {
394
+ if (typeof value === 'string') return stringifyJsonValue({ input: value }, '{"input":""}');
395
+ if (value == null) return '{"input":""}';
396
+ if (isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'input')) {
397
+ return stringifyJsonValue(value, '{"input":""}');
312
398
  }
399
+ return stringifyJsonValue({ input: normalizeResponsesToolOutput(value) }, '{"input":""}');
400
+ }
313
401
 
402
+ function toOpenAiToolCall(item, fallbackIndex) {
403
+ if (!isRecord(item)) return null;
404
+ const callId = asTrimmedString(item.call_id || item.id) || `call_${crypto.randomBytes(8).toString('hex')}_${fallbackIndex}`;
405
+ const type = asTrimmedString(item.type).toLowerCase();
406
+ const name = asTrimmedString(item.name)
407
+ || asTrimmedString(item.server_label)
408
+ || (type === 'local_shell_call' ? 'local_shell' : '');
409
+ if (!name) return null;
410
+ const rawArguments = item.arguments != null
411
+ ? item.arguments
412
+ : (item.input != null ? item.input : (item.action != null ? item.action : item.command));
413
+ const args = (type === 'custom_tool_call' && item.arguments == null)
414
+ ? normalizeFreeformToolArguments(rawArguments)
415
+ : normalizeOpenAiToolArguments(rawArguments);
416
+ return {
417
+ id: callId,
418
+ type: 'function',
419
+ function: {
420
+ name,
421
+ arguments: args
422
+ }
423
+ };
424
+ }
425
+
426
+ function hasOpenAiMessageContent(content) {
427
+ return typeof content === 'string'
428
+ ? content.trim().length > 0
429
+ : Array.isArray(content) && content.length > 0;
430
+ }
431
+
432
+ function normalizeResponsesInputToChatMessages(input) {
433
+ // Keep the OpenAI bridge in lockstep with the builtin proxy's Responses → Chat shim.
434
+ // Codex long-running tasks append richer Responses history (custom/local_shell/MCP calls)
435
+ // back into `input`; dropping those items makes the next model turn lose tool state and stop early.
314
436
  const messages = [];
315
- for (const item of input) {
316
- if (!item || typeof item !== 'object') continue;
437
+ const normalizedInput = stripOrphanedResponsesToolOutputs(input);
438
+ let functionCallIndex = 0;
439
+ let pendingToolCalls = [];
440
+ const emittedToolCallIds = new Set();
441
+
442
+ const flushPendingToolCalls = () => {
443
+ if (pendingToolCalls.length <= 0) return;
444
+ for (const toolCall of pendingToolCalls) {
445
+ const callId = asTrimmedString(toolCall.id);
446
+ if (callId) emittedToolCallIds.add(callId);
447
+ }
448
+ messages.push({
449
+ role: 'assistant',
450
+ content: null,
451
+ tool_calls: pendingToolCalls
452
+ });
453
+ pendingToolCalls = [];
454
+ };
317
455
 
318
- // Tool calls (Responses): { type: "function_call", call_id, name, arguments }
319
- // Chat Completions equivalent: assistant message with tool_calls
320
- if (typeof item.type === 'string' && item.type === 'function_call') {
321
- const callId = typeof item.call_id === 'string' ? item.call_id.trim() : '';
322
- const name = typeof item.name === 'string' ? item.name.trim() : '';
323
- const args = typeof item.arguments === 'string' ? item.arguments : '';
324
- if (callId && name) {
325
- messages.push({
326
- role: 'assistant',
327
- tool_calls: [{
328
- id: callId,
329
- type: 'function',
330
- function: { name, arguments: args || '' }
331
- }]
332
- });
333
- }
334
- continue;
456
+ const pushToolOutputMessage = (callIdRaw, outputRaw) => {
457
+ const toolCallId = asTrimmedString(callIdRaw);
458
+ if (!toolCallId) return;
459
+ messages.push({
460
+ role: 'tool',
461
+ tool_call_id: toolCallId,
462
+ content: normalizeResponsesToolOutput(outputRaw)
463
+ });
464
+ };
465
+
466
+ const processInputItem = (item) => {
467
+ if (typeof item === 'string') {
468
+ flushPendingToolCalls();
469
+ const text = item.trim();
470
+ if (text) messages.push({ role: 'user', content: text });
471
+ return;
472
+ }
473
+ if (!isRecord(item)) return;
474
+
475
+ const itemType = asTrimmedString(item.type).toLowerCase();
476
+ if (RESPONSES_TOOL_CALL_INPUT_TYPES.has(itemType)) {
477
+ const toolCall = toOpenAiToolCall(item, functionCallIndex);
478
+ functionCallIndex += 1;
479
+ if (toolCall) pendingToolCalls.push(toolCall);
480
+ return;
335
481
  }
336
482
 
337
- // Tool results (Responses): { type: "function_call_output", call_id, output }
338
- // Chat Completions equivalent: { role: "tool", tool_call_id, content }
339
- if (typeof item.type === 'string' && item.type === 'function_call_output') {
340
- const toolCallId = typeof item.call_id === 'string' ? item.call_id.trim() : '';
341
- let content = item.output;
342
- if (typeof content !== 'string') {
343
- try {
344
- content = JSON.stringify(content);
345
- } catch (_) {
346
- content = String(content ?? '');
347
- }
348
- }
349
- if (toolCallId) {
350
- messages.push({ role: 'tool', tool_call_id: toolCallId, content: String(content || '') });
483
+ if (RESPONSES_TOOL_CALL_OUTPUT_TYPES.has(itemType)) {
484
+ flushPendingToolCalls();
485
+ const toolCallId = asTrimmedString(item.call_id || item.id);
486
+ if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
487
+ pushToolOutputMessage(toolCallId, item.output != null ? item.output : item.content);
488
+ return;
489
+ }
490
+
491
+ if (itemType === 'reasoning') {
492
+ flushPendingToolCalls();
493
+ const reasoningContent = toOpenAiMessageContent(item.summary != null ? item.summary : (item.content != null ? item.content : item));
494
+ const reasoningSignature = asTrimmedString(item.encrypted_content || item.reasoning_signature);
495
+ if (!hasOpenAiMessageContent(reasoningContent) && !reasoningSignature) return;
496
+ const message = { role: 'assistant', content: reasoningContent };
497
+ if (reasoningSignature) message.reasoning_signature = reasoningSignature;
498
+ messages.push(message);
499
+ return;
500
+ }
501
+
502
+ flushPendingToolCalls();
503
+ const role = asTrimmedString(item.role).toLowerCase() || 'user';
504
+ const normalizedRole = role === 'developer' ? 'system' : role;
505
+ const content = toOpenAiMessageContent(item.content != null ? item.content : (item.input != null ? item.input : item));
506
+
507
+ if (normalizedRole === 'tool') {
508
+ const toolCallId = asTrimmedString(item.tool_call_id || item.call_id || item.id);
509
+ if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
510
+ pushToolOutputMessage(toolCallId, item.content);
511
+ return;
512
+ }
513
+
514
+ if (!hasOpenAiMessageContent(content)) return;
515
+ const message = { role: normalizedRole, content };
516
+ const phase = asTrimmedString(item.phase);
517
+ if (phase) message.phase = phase;
518
+ messages.push(message);
519
+ };
520
+
521
+ if (typeof normalizedInput === 'string') {
522
+ const text = normalizedInput.trim();
523
+ if (text) messages.push({ role: 'user', content: text });
524
+ } else if (Array.isArray(normalizedInput)) {
525
+ for (const item of normalizedInput) processInputItem(item);
526
+ } else if (isRecord(normalizedInput)) {
527
+ processInputItem(normalizedInput);
528
+ }
529
+ flushPendingToolCalls();
530
+ return messages;
531
+ }
532
+
533
+ function normalizeFunctionToolForChat(tool) {
534
+ if (!isRecord(tool)) return null;
535
+ const sourceFn = isRecord(tool.function) ? tool.function : tool;
536
+ const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
537
+ if (!name) return null;
538
+ const fn = { name };
539
+ const description = asTrimmedString(sourceFn.description) || asTrimmedString(tool.description);
540
+ if (description) fn.description = description;
541
+ if (sourceFn.parameters !== undefined) {
542
+ fn.parameters = cloneJsonValue(sourceFn.parameters);
543
+ } else if (tool.parameters !== undefined) {
544
+ fn.parameters = cloneJsonValue(tool.parameters);
545
+ }
546
+ if (typeof sourceFn.strict === 'boolean') {
547
+ fn.strict = sourceFn.strict;
548
+ } else if (typeof tool.strict === 'boolean') {
549
+ fn.strict = tool.strict;
550
+ }
551
+ return { type: 'function', function: fn };
552
+ }
553
+
554
+ function buildLocalShellToolForChat(tool) {
555
+ return {
556
+ type: 'function',
557
+ function: {
558
+ name: asTrimmedString(tool && tool.name) || 'local_shell',
559
+ description: asTrimmedString(tool && tool.description) || 'Run a local shell command and return its output.',
560
+ parameters: {
561
+ type: 'object',
562
+ properties: {
563
+ cmd: { type: 'string', description: 'Shell command to execute.' },
564
+ yield_time_ms: { type: 'number', description: 'Milliseconds to wait before yielding partial output.' },
565
+ max_output_tokens: { type: 'number', description: 'Maximum output tokens to return.' }
566
+ },
567
+ required: ['cmd'],
568
+ additionalProperties: true
351
569
  }
352
- continue;
353
570
  }
571
+ };
572
+ }
354
573
 
355
- // message form
356
- if (typeof item.role === 'string' && item.content != null) {
357
- const role = toRole(item.role);
358
- const content = Array.isArray(item.content)
359
- ? toChatContent(item.content)
360
- : item.content;
361
- if (content) {
362
- messages.push({ role, content });
574
+ function buildFreeformToolForChat(tool, fallbackName = 'custom_tool') {
575
+ return {
576
+ type: 'function',
577
+ function: {
578
+ name: asTrimmedString(tool && tool.name) || fallbackName,
579
+ description: asTrimmedString(tool && tool.description) || 'Pass raw freeform input to the local tool.',
580
+ parameters: {
581
+ type: 'object',
582
+ properties: {
583
+ input: { type: 'string', description: 'Raw tool input.' }
584
+ },
585
+ required: ['input'],
586
+ additionalProperties: false
363
587
  }
364
- continue;
365
588
  }
589
+ };
590
+ }
591
+
592
+ const MAX_RESPONSES_TOOL_NAMESPACE_DEPTH = 5;
593
+
594
+ function rememberResponsesToolType(tool, target, depth = 0) {
595
+ if (!isRecord(tool) || !target || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return;
596
+ const type = asTrimmedString(tool.type).toLowerCase();
597
+ if (type === 'namespace' && Array.isArray(tool.tools)) {
598
+ for (const inner of tool.tools) rememberResponsesToolType(inner, target, depth + 1);
599
+ return;
366
600
  }
601
+ const sourceFn = isRecord(tool.function) ? tool.function : tool;
602
+ const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
603
+ if (!name) return;
604
+ if (type === 'local_shell') {
605
+ target[name] = 'local_shell_call';
606
+ return;
607
+ }
608
+ if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
609
+ target[name] = 'custom_tool_call';
610
+ return;
611
+ }
612
+ if (type === 'function') {
613
+ target[name] = 'function_call';
614
+ }
615
+ }
367
616
 
368
- if (messages.length > 0) {
369
- return messages;
617
+ function collectResponsesToolTypesByName(tools) {
618
+ const result = {};
619
+ if (!Array.isArray(tools)) return result;
620
+ for (const tool of tools) rememberResponsesToolType(tool, result);
621
+ return result;
622
+ }
623
+
624
+ function extractFreeformInputFromChatArguments(argumentsText) {
625
+ if (typeof argumentsText !== 'string') return '';
626
+ const parsed = parseJsonValueOrNull(argumentsText);
627
+ if (isRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, 'input')) {
628
+ return typeof parsed.input === 'string' ? parsed.input : normalizeResponsesToolOutput(parsed.input);
370
629
  }
630
+ return argumentsText;
631
+ }
371
632
 
372
- // 退化:把 input array 当作单条 user content blocks
373
- const fallbackContent = toChatContent(input);
374
- if (fallbackContent) {
375
- return [{ role: 'user', content: fallbackContent }];
633
+ function extractLocalShellActionFromChatArguments(argumentsText) {
634
+ const parsed = parseJsonValueOrNull(argumentsText);
635
+ if (isRecord(parsed)) return cloneJsonValue(parsed);
636
+ return { cmd: typeof argumentsText === 'string' ? argumentsText : '' };
637
+ }
638
+
639
+ function buildResponsesToolCallItemFromChatToolCall(toolCall, toolTypesByName = {}) {
640
+ if (!isRecord(toolCall)) return null;
641
+ const fn = isRecord(toolCall.function) ? toolCall.function : {};
642
+ const name = asTrimmedString(fn.name);
643
+ if (!name) return null;
644
+ const callId = asTrimmedString(toolCall.id) || `call_${crypto.randomBytes(8).toString('hex')}`;
645
+ const argumentsText = typeof fn.arguments === 'string' ? fn.arguments : '';
646
+ const responseType = toolTypesByName && toolTypesByName[name] ? toolTypesByName[name] : 'function_call';
647
+
648
+ if (responseType === 'custom_tool_call') {
649
+ return {
650
+ type: 'custom_tool_call',
651
+ call_id: callId,
652
+ name,
653
+ input: extractFreeformInputFromChatArguments(argumentsText)
654
+ };
376
655
  }
377
- return [];
656
+ if (responseType === 'local_shell_call') {
657
+ return {
658
+ type: 'local_shell_call',
659
+ call_id: callId,
660
+ name,
661
+ action: extractLocalShellActionFromChatArguments(argumentsText)
662
+ };
663
+ }
664
+ return {
665
+ type: 'function_call',
666
+ call_id: callId,
667
+ name,
668
+ arguments: argumentsText
669
+ };
378
670
  }
379
671
 
672
+ function normalizeSingleResponsesToolToChatTools(tool, depth = 0) {
673
+ if (!isRecord(tool) || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return [];
674
+ const type = asTrimmedString(tool.type).toLowerCase();
675
+ if (type === 'namespace' && Array.isArray(tool.tools)) {
676
+ return tool.tools.flatMap((inner) => normalizeSingleResponsesToolToChatTools(inner, depth + 1));
677
+ }
678
+ if (type === 'function') {
679
+ const converted = normalizeFunctionToolForChat(tool);
680
+ return converted ? [converted] : [];
681
+ }
682
+ if (type === 'local_shell') {
683
+ return [buildLocalShellToolForChat(tool)];
684
+ }
685
+ const name = asTrimmedString(tool.name);
686
+ if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
687
+ return [buildFreeformToolForChat(tool, name || 'custom_tool')];
688
+ }
689
+ return [];
690
+ }
380
691
 
381
692
  function normalizeResponsesToolsToChatTools(tools) {
382
693
  if (!Array.isArray(tools)) return tools;
383
- return tools
384
- .map((tool) => {
385
- if (!tool || typeof tool !== 'object') return null;
386
- if (tool.type !== 'function') return null;
387
- const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
388
- ? tool.function
389
- : {};
390
- const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
391
- ? sourceFn.name.trim()
392
- : (typeof tool.name === 'string' ? tool.name.trim() : '');
393
- if (!name) return null;
394
- const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
395
- ? sourceFn.parameters
396
- : (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : {});
397
- const fn = { name, parameters };
398
- const description = typeof sourceFn.description === 'string'
399
- ? sourceFn.description
400
- : (typeof tool.description === 'string' ? tool.description : undefined);
401
- const strict = typeof sourceFn.strict === 'boolean'
402
- ? sourceFn.strict
403
- : (typeof tool.strict === 'boolean' ? tool.strict : undefined);
404
- if (description !== undefined) fn.description = description;
405
- if (strict !== undefined) fn.strict = strict;
406
- return { type: 'function', function: fn };
407
- })
408
- .filter(Boolean);
694
+ return tools.flatMap((tool) => normalizeSingleResponsesToolToChatTools(tool));
409
695
  }
410
696
 
411
697
  function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
412
- if (!toolChoice || typeof toolChoice !== 'object' || Array.isArray(toolChoice)) return toolChoice;
413
- if (toolChoice.type === 'function' && typeof toolChoice.name === 'string' && toolChoice.name.trim()) {
414
- return { type: 'function', function: { name: toolChoice.name.trim() } };
698
+ if (toolChoice === undefined) return undefined;
699
+ if (typeof toolChoice === 'string') return toolChoice;
700
+ if (!isRecord(toolChoice)) return toolChoice;
701
+
702
+ const type = asTrimmedString(toolChoice.type).toLowerCase();
703
+ if (type === 'tool' || type === 'function' || type === 'custom' || type === 'custom_tool' || type === 'local_shell') {
704
+ if (isRecord(toolChoice.function) && asTrimmedString(toolChoice.function.name)) return cloneJsonValue(toolChoice);
705
+ const name = asTrimmedString(toolChoice.name) || asTrimmedString(toolChoice.server_label);
706
+ if (!name) return 'required';
707
+ return { type: 'function', function: { name } };
708
+ }
709
+ if (type === 'auto' || type === 'none' || type === 'required') return type;
710
+ return 'auto';
711
+ }
712
+
713
+ function getChatToolChoiceName(toolChoice) {
714
+ if (!isRecord(toolChoice)) return '';
715
+ if (isRecord(toolChoice.function)) return asTrimmedString(toolChoice.function.name);
716
+ return '';
717
+ }
718
+
719
+ function pruneInvalidChatToolChoice(chatBody) {
720
+ if (!isRecord(chatBody) || !Array.isArray(chatBody.tools)) return;
721
+ if (chatBody.tools.length === 0) {
722
+ delete chatBody.tools;
723
+ delete chatBody.tool_choice;
724
+ return;
725
+ }
726
+ const chosenName = getChatToolChoiceName(chatBody.tool_choice);
727
+ if (!chosenName) return;
728
+ const toolNames = new Set(chatBody.tools
729
+ .map((tool) => isRecord(tool) && isRecord(tool.function) ? asTrimmedString(tool.function.name) : '')
730
+ .filter(Boolean));
731
+ if (!toolNames.has(chosenName)) {
732
+ delete chatBody.tool_choice;
415
733
  }
416
- return toolChoice;
417
734
  }
418
735
 
419
736
  function normalizeResponsesToolsForResponsesApi(tools) {
420
737
  if (!Array.isArray(tools)) return tools;
421
738
  return tools
422
739
  .map((tool) => {
423
- if (!tool || typeof tool !== 'object') return null;
424
- if (tool.type !== 'function') return null;
425
- const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
426
- ? tool.function
427
- : {};
428
- const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
429
- ? sourceFn.name.trim()
430
- : (typeof tool.name === 'string' ? tool.name.trim() : '');
431
- if (!name) return null;
432
- const out = { type: 'function', name };
433
- const description = typeof sourceFn.description === 'string'
434
- ? sourceFn.description
435
- : (typeof tool.description === 'string' ? tool.description : undefined);
436
- const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
437
- ? sourceFn.parameters
438
- : (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : undefined);
439
- const strict = typeof sourceFn.strict === 'boolean'
440
- ? sourceFn.strict
441
- : (typeof tool.strict === 'boolean' ? tool.strict : undefined);
442
- if (description !== undefined) out.description = description;
443
- if (parameters !== undefined) out.parameters = parameters;
444
- if (strict !== undefined) out.strict = strict;
740
+ const converted = normalizeFunctionToolForChat(tool);
741
+ if (!converted || !converted.function) return null;
742
+ const out = {
743
+ type: 'function',
744
+ name: converted.function.name
745
+ };
746
+ if (converted.function.description !== undefined) out.description = converted.function.description;
747
+ if (converted.function.parameters !== undefined) out.parameters = converted.function.parameters;
748
+ if (converted.function.strict !== undefined) out.strict = converted.function.strict;
445
749
  return out;
446
750
  })
447
751
  .filter(Boolean);
@@ -484,6 +788,39 @@ function mergeLeadingSystemMessages(messages, leadingInstructions) {
484
788
  return out;
485
789
  }
486
790
 
791
+ function messageContentAsText(content) {
792
+ if (typeof content === 'string') return content;
793
+ if (!Array.isArray(content)) return '';
794
+ return content
795
+ .map((item) => {
796
+ if (typeof item === 'string') return item;
797
+ if (!isRecord(item)) return '';
798
+ if (typeof item.text === 'string') return item.text;
799
+ if (typeof item.content === 'string') return item.content;
800
+ return '';
801
+ })
802
+ .filter(Boolean)
803
+ .join('\n');
804
+ }
805
+
806
+ function hasRunningCodexExecSession(messages) {
807
+ if (!Array.isArray(messages)) return false;
808
+ return messages.some((message) => {
809
+ if (!isRecord(message) || message.role !== 'tool') return false;
810
+ return /Process running with session ID\s+\d+/i.test(messageContentAsText(message.content));
811
+ });
812
+ }
813
+
814
+ function appendChatFallbackRuntimeInstructions(baseInstructions, rawMessages) {
815
+ const segments = [];
816
+ const base = typeof baseInstructions === 'string' ? baseInstructions.trim() : '';
817
+ if (base) segments.push(base);
818
+ if (hasRunningCodexExecSession(rawMessages)) {
819
+ segments.push('Codex tool output indicates a command is still running ("Process running with session ID ..."). You must call write_stdin with that numeric session_id and empty chars to poll/wait for completion before giving a final answer. Do not merely say that you are waiting.');
820
+ }
821
+ return segments.join('\n\n');
822
+ }
823
+
487
824
  function convertResponsesRequestToChatCompletions(payload) {
488
825
  const body = payload && typeof payload === 'object' ? payload : {};
489
826
  const model = typeof body.model === 'string' ? body.model.trim() : '';
@@ -492,9 +829,10 @@ function convertResponsesRequestToChatCompletions(payload) {
492
829
  }
493
830
 
494
831
  const rawMessages = normalizeResponsesInputToChatMessages(body.input);
832
+ const leadingInstructions = appendChatFallbackRuntimeInstructions(body.instructions, rawMessages);
495
833
  // codex 同时下发 body.instructions(内置 prompt)与 input 内 developer/system 消息(AGENTS.md)。
496
834
  // 合流为一条领头 system,避免某些上游"只认第一条 system"导致 AGENTS.md 失效。
497
- const messages = mergeLeadingSystemMessages(rawMessages, body.instructions);
835
+ const messages = mergeLeadingSystemMessages(rawMessages, leadingInstructions);
498
836
  if (!messages.length) {
499
837
  // codex sometimes sends empty input for probes; tolerate.
500
838
  messages.push({ role: 'user', content: '' });
@@ -527,13 +865,15 @@ function convertResponsesRequestToChatCompletions(payload) {
527
865
  chat.metadata = body.metadata;
528
866
  }
529
867
 
868
+ pruneInvalidChatToolChoice(chat);
869
+
530
870
  // Remove undefined keys
531
871
  Object.keys(chat).forEach((key) => chat[key] === undefined && delete chat[key]);
532
872
 
533
- return { chat, streamRequested: stream };
873
+ return { chat, streamRequested: stream, toolTypesByName: collectResponsesToolTypesByName(body.tools) };
534
874
  }
535
875
 
536
- function buildResponsesPayloadFromChatResult(model, text, toolCalls, upstreamPayload) {
876
+ function buildResponsesPayloadFromChatResult(model, text, toolCalls, upstreamPayload, options = {}) {
537
877
  const responseId = `resp_${crypto.randomBytes(10).toString('hex')}`;
538
878
  const usage = upstreamPayload && upstreamPayload.usage && typeof upstreamPayload.usage === 'object'
539
879
  ? upstreamPayload.usage
@@ -550,22 +890,13 @@ function buildResponsesPayloadFromChatResult(model, text, toolCalls, upstreamPay
550
890
  });
551
891
  }
552
892
 
553
- // Convert chat.completions tool_calls into Responses-style function_call output items.
554
- // This is important for Codex, which appends function_call + function_call_output back into `input`.
893
+ // Convert chat.completions tool_calls back into the original Responses item type.
894
+ // Treating every call as `function_call` makes Codex built-ins (custom/local_shell)
895
+ // degrade into ordinary chat text instead of executable agent steps.
555
896
  if (Array.isArray(toolCalls)) {
556
897
  for (const call of toolCalls) {
557
- if (!call || typeof call !== 'object') continue;
558
- const callId = typeof call.id === 'string' && call.id.trim() ? call.id.trim() : `call_${crypto.randomBytes(8).toString('hex')}`;
559
- const fn = call.function && typeof call.function === 'object' ? call.function : {};
560
- const name = typeof fn.name === 'string' ? fn.name : '';
561
- const args = typeof fn.arguments === 'string' ? fn.arguments : '';
562
- if (!name) continue;
563
- output.push({
564
- type: 'function_call',
565
- call_id: callId,
566
- name,
567
- arguments: args
568
- });
898
+ const item = buildResponsesToolCallItemFromChatToolCall(call, options.toolTypesByName || {});
899
+ if (item) output.push(item);
569
900
  }
570
901
  }
571
902
 
@@ -932,14 +1263,8 @@ function finishChatStreamResponsesSse(state) {
932
1263
 
933
1264
  for (const toolCall of state.toolCalls) {
934
1265
  if (!toolCall) continue;
935
- const name = toolCall.function && typeof toolCall.function.name === 'string' ? toolCall.function.name : '';
936
- if (!name) continue;
937
- const item = {
938
- type: 'function_call',
939
- call_id: toolCall.id || `call_${crypto.randomBytes(8).toString('hex')}`,
940
- name,
941
- arguments: toolCall.function && typeof toolCall.function.arguments === 'string' ? toolCall.function.arguments : ''
942
- };
1266
+ const item = buildResponsesToolCallItemFromChatToolCall(toolCall, state.toolTypesByName || {});
1267
+ if (!item) continue;
943
1268
  const outputIndex = state.output.length;
944
1269
  state.output.push(item);
945
1270
  writeSse(state.res, 'response.output_item.added', {
@@ -1102,7 +1427,9 @@ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
1102
1427
  return;
1103
1428
  }
1104
1429
  const extracted = extractChatCompletionResult(parsedJson.value);
1105
- sendResponsesSse(res, buildResponsesPayloadFromChatResult(fallbackModel, extracted.text, extracted.toolCalls, parsedJson.value));
1430
+ sendResponsesSse(res, buildResponsesPayloadFromChatResult(fallbackModel, extracted.text, extracted.toolCalls, parsedJson.value, {
1431
+ toolTypesByName: options.toolTypesByName || {}
1432
+ }));
1106
1433
  if (!res.writableEnded && !res.destroyed) res.end();
1107
1434
  finish({ ok: true });
1108
1435
  });
@@ -1119,6 +1446,7 @@ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
1119
1446
  messageItem: null,
1120
1447
  messageText: '',
1121
1448
  toolCalls: [],
1449
+ toolTypesByName: options.toolTypesByName || {},
1122
1450
  finished: false,
1123
1451
  sawDone: false,
1124
1452
  sawFinishReason: false,
@@ -1475,7 +1803,8 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1475
1803
  httpAgent,
1476
1804
  httpsAgent,
1477
1805
  res,
1478
- model: typeof chatBody.model === 'string' ? chatBody.model : ''
1806
+ model: typeof chatBody.model === 'string' ? chatBody.model : '',
1807
+ toolTypesByName: converted.toolTypesByName || {}
1479
1808
  }));
1480
1809
  if (!streamed.ok) {
1481
1810
  if (res.writableEnded || res.destroyed) {
@@ -1598,7 +1927,9 @@ function createOpenaiBridgeHttpHandler(options = {}) {
1598
1927
  const extracted = extractChatCompletionResult(upstreamJson.value);
1599
1928
  const text = extracted && typeof extracted.text === 'string' ? extracted.text : '';
1600
1929
  const toolCalls = extracted && Array.isArray(extracted.toolCalls) ? extracted.toolCalls : [];
1601
- const responsesPayload = buildResponsesPayloadFromChatResult(model, text, toolCalls, upstreamJson.value);
1930
+ const responsesPayload = buildResponsesPayloadFromChatResult(model, text, toolCalls, upstreamJson.value, {
1931
+ toolTypesByName: converted.toolTypesByName || {}
1932
+ });
1602
1933
 
1603
1934
  if (converted.streamRequested && wantsSse) {
1604
1935
  res.writeHead(200, {