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.
- package/cli/builtin-proxy.js +626 -207
- package/cli/config-bootstrap.js +6 -1
- package/cli/openai-bridge.js +541 -210
- package/cli.js +189 -4
- package/package.json +1 -1
- package/plugins/prompt-templates/computed.mjs +61 -3
- package/plugins/prompt-templates/manifest.mjs +3 -0
- package/web-ui/app.js +14 -3
- package/web-ui/modules/app.computed.main-tabs.mjs +39 -30
- package/web-ui/modules/app.methods.claude-config.mjs +111 -9
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
- package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
- package/web-ui/modules/app.methods.providers.mjs +36 -10
- package/web-ui/modules/app.methods.runtime.mjs +76 -1
- package/web-ui/modules/app.methods.startup-claude.mjs +7 -0
- package/web-ui/modules/app.methods.tool-config-permissions.mjs +87 -0
- package/web-ui/modules/config-mode.computed.mjs +3 -3
- package/web-ui/modules/i18n/locales/en.mjs +1140 -0
- package/web-ui/modules/i18n/locales/ja.mjs +1130 -0
- package/web-ui/modules/i18n/locales/vi.mjs +239 -0
- package/web-ui/modules/i18n/locales/zh.mjs +1143 -0
- package/web-ui/modules/i18n.dict.mjs +9 -3195
- package/web-ui/modules/i18n.mjs +65 -16
- package/web-ui/partials/index/layout-header.html +16 -46
- package/web-ui/partials/index/modal-openclaw-config.html +135 -71
- package/web-ui/partials/index/modal-webhook.html +8 -8
- package/web-ui/partials/index/modals-basic.html +56 -16
- package/web-ui/partials/index/panel-config-claude.html +51 -21
- package/web-ui/partials/index/panel-config-codex.html +34 -5
- package/web-ui/partials/index/panel-config-openclaw.html +70 -64
- package/web-ui/partials/index/panel-dashboard.html +62 -77
- package/web-ui/partials/index/panel-settings.html +28 -7
- package/web-ui/partials/index/panel-trash.html +14 -14
- package/web-ui/res/web-ui-render.precompiled.js +1783 -1386
- package/web-ui/styles/controls-forms.css +99 -0
- package/web-ui/styles/dashboard.css +46 -14
- package/web-ui/styles/layout-shell.css +45 -0
- package/web-ui/styles/navigation-panels.css +3 -3
- package/web-ui/styles/openclaw-structured.css +383 -33
- package/web-ui/styles/responsive.css +68 -0
- package/web-ui/styles/sessions-usage.css +105 -9
- package/web-ui/styles/settings-panel.css +4 -0
package/cli/builtin-proxy.js
CHANGED
|
@@ -138,7 +138,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
138
138
|
return false;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
const TRANSIENT_RETRY_DELAYS_MS = [200, 600];
|
|
141
|
+
const TRANSIENT_RETRY_DELAYS_MS = [200, 600, 1200];
|
|
142
142
|
|
|
143
143
|
async function retryTransientRequest(executor) {
|
|
144
144
|
let lastResult = null;
|
|
@@ -158,7 +158,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
158
158
|
if (result.ok) return result;
|
|
159
159
|
if (result.retry) return result;
|
|
160
160
|
if (result.status && result.status > 0) return result;
|
|
161
|
-
if (!isTransientNetworkError(result.error)) return result;
|
|
161
|
+
if (!result.retryTransient && !isTransientNetworkError(result.error)) return result;
|
|
162
162
|
}
|
|
163
163
|
return lastResult;
|
|
164
164
|
}
|
|
@@ -264,6 +264,17 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
function parseJsonValueOrNull(value) {
|
|
268
|
+
if (typeof value !== 'string') return null;
|
|
269
|
+
const text = value.trim();
|
|
270
|
+
if (!text) return null;
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(text);
|
|
273
|
+
} catch (_) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
267
278
|
function normalizeChatUsageToResponsesUsage(usage) {
|
|
268
279
|
if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return undefined;
|
|
269
280
|
const pickNumber = (...keys) => {
|
|
@@ -338,7 +349,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
338
349
|
return blocks;
|
|
339
350
|
}
|
|
340
351
|
|
|
341
|
-
function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '') {
|
|
352
|
+
function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '', options = {}) {
|
|
342
353
|
const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
|
343
354
|
const choice = Array.isArray(base.choices) ? base.choices[0] : null;
|
|
344
355
|
const message = choice && typeof choice === 'object' && choice.message && typeof choice.message === 'object'
|
|
@@ -355,16 +366,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
355
366
|
}
|
|
356
367
|
if (Array.isArray(message.tool_calls)) {
|
|
357
368
|
for (const toolCall of message.tool_calls) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const name = typeof fn.name === 'string' ? fn.name : '';
|
|
361
|
-
if (!name) continue;
|
|
362
|
-
output.push({
|
|
363
|
-
type: 'function_call',
|
|
364
|
-
call_id: typeof toolCall.id === 'string' && toolCall.id ? toolCall.id : `call_${crypto.randomBytes(8).toString('hex')}`,
|
|
365
|
-
name,
|
|
366
|
-
arguments: stringifyJsonValue(fn.arguments, '{}')
|
|
367
|
-
});
|
|
369
|
+
const item = buildResponsesToolCallItemFromChatToolCall(toolCall, options.toolTypesByName || {});
|
|
370
|
+
if (item) output.push(item);
|
|
368
371
|
}
|
|
369
372
|
}
|
|
370
373
|
const finish = mapChatFinishReasonToResponses(choice);
|
|
@@ -378,151 +381,499 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
378
381
|
});
|
|
379
382
|
}
|
|
380
383
|
|
|
384
|
+
function isRecord(value) {
|
|
385
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function asTrimmedString(value) {
|
|
389
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function cloneJsonValue(value) {
|
|
393
|
+
if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
|
|
394
|
+
if (isRecord(value)) {
|
|
395
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneJsonValue(item)]));
|
|
396
|
+
}
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function normalizeResponsesToolOutput(value) {
|
|
401
|
+
if (typeof value === 'string') return value;
|
|
402
|
+
if (value == null) return '';
|
|
403
|
+
return stringifyJsonValue(value, '');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function normalizeOpenAiToolArguments(value) {
|
|
407
|
+
if (typeof value === 'string') return value;
|
|
408
|
+
if (value == null) return '{}';
|
|
409
|
+
return stringifyJsonValue(value, '{}');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function normalizeInputFileBlock(item) {
|
|
413
|
+
if (!isRecord(item)) return null;
|
|
414
|
+
const file = isRecord(item.file) ? item.file : item;
|
|
415
|
+
const out = {};
|
|
416
|
+
const fileId = asTrimmedString(file.file_id || file.id);
|
|
417
|
+
const filename = asTrimmedString(file.filename || file.name);
|
|
418
|
+
const fileData = asTrimmedString(file.file_data || file.data);
|
|
419
|
+
const mimeType = asTrimmedString(file.mime_type || file.media_type);
|
|
420
|
+
if (fileId) out.file_id = fileId;
|
|
421
|
+
if (filename) out.filename = filename;
|
|
422
|
+
if (fileData) out.file_data = fileData;
|
|
423
|
+
if (mimeType) out.mime_type = mimeType;
|
|
424
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizeResponsesContentBlockForChat(item) {
|
|
428
|
+
if (typeof item === 'string') return item.trim() ? item : null;
|
|
429
|
+
if (!isRecord(item)) return null;
|
|
430
|
+
|
|
431
|
+
const type = asTrimmedString(item.type).toLowerCase();
|
|
432
|
+
if (!type) {
|
|
433
|
+
const text = asTrimmedString(item.text || item.content || item.output_text);
|
|
434
|
+
return text ? { type: 'text', text } : null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (type === 'input_text' || type === 'output_text' || type === 'text' || type === 'summary_text' || type === 'reasoning_text') {
|
|
438
|
+
const text = typeof item.text === 'string' ? item.text : asTrimmedString(item.content || item.output_text);
|
|
439
|
+
return text ? { type: 'text', text } : null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (type === 'refusal' && typeof item.refusal === 'string') {
|
|
443
|
+
return item.refusal ? { type: 'text', text: item.refusal } : null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (type === 'input_image') {
|
|
447
|
+
const raw = item.image_url != null ? item.image_url : (item.url != null ? item.url : item.imageUrl);
|
|
448
|
+
if (raw === undefined) return null;
|
|
449
|
+
return {
|
|
450
|
+
type: 'image_url',
|
|
451
|
+
image_url: typeof raw === 'string' ? { url: raw } : cloneJsonValue(raw)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (type === 'image_url' && item.image_url !== undefined) {
|
|
456
|
+
return { type: 'image_url', image_url: item.image_url };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (type === 'input_audio') {
|
|
460
|
+
if (item.input_audio !== undefined) return { type: 'input_audio', input_audio: item.input_audio };
|
|
461
|
+
if (item.data !== undefined || item.format !== undefined) {
|
|
462
|
+
return { type: 'input_audio', input_audio: { data: item.data, format: item.format } };
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (type === 'input_file' || type === 'file') {
|
|
468
|
+
const file = normalizeInputFileBlock(item);
|
|
469
|
+
return file ? { type: 'file', file } : null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (type === 'reasoning' || type === 'thinking' || type === 'redacted_reasoning') {
|
|
473
|
+
const text = asTrimmedString(item.text || item.content);
|
|
474
|
+
return text ? { type: 'text', text } : null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return cloneJsonValue(item);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function toOpenAiMessageContent(content) {
|
|
481
|
+
if (typeof content === 'string') return content;
|
|
482
|
+
if (!Array.isArray(content)) {
|
|
483
|
+
if (isRecord(content)) {
|
|
484
|
+
const single = normalizeResponsesContentBlockForChat(content);
|
|
485
|
+
if (!single) return '';
|
|
486
|
+
return typeof single === 'string' ? single : [single];
|
|
487
|
+
}
|
|
488
|
+
return '';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const blocks = content
|
|
492
|
+
.map((item) => normalizeResponsesContentBlockForChat(item))
|
|
493
|
+
.filter((item) => !!item);
|
|
494
|
+
|
|
495
|
+
if (blocks.length === 0) return '';
|
|
496
|
+
if (blocks.length === 1 && typeof blocks[0] === 'string') return blocks[0];
|
|
497
|
+
return blocks;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const RESPONSES_TOOL_CALL_INPUT_TYPES = new Set(['function_call', 'custom_tool_call', 'mcp_tool_call', 'local_shell_call']);
|
|
501
|
+
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']);
|
|
502
|
+
|
|
503
|
+
function stripOrphanedResponsesToolOutputs(input) {
|
|
504
|
+
if (!Array.isArray(input)) return input;
|
|
505
|
+
const seenToolCallIds = new Set();
|
|
506
|
+
const sanitized = [];
|
|
507
|
+
for (const item of input) {
|
|
508
|
+
if (!isRecord(item)) {
|
|
509
|
+
sanitized.push(item);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const type = asTrimmedString(item.type).toLowerCase();
|
|
513
|
+
if (RESPONSES_TOOL_CALL_INPUT_TYPES.has(type)) {
|
|
514
|
+
const callId = asTrimmedString(item.call_id || item.id);
|
|
515
|
+
if (callId) seenToolCallIds.add(callId);
|
|
516
|
+
sanitized.push(item);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (RESPONSES_TOOL_CALL_OUTPUT_TYPES.has(type)) {
|
|
520
|
+
const callId = asTrimmedString(item.call_id || item.id);
|
|
521
|
+
if (!callId || !seenToolCallIds.has(callId)) continue;
|
|
522
|
+
sanitized.push(item);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
sanitized.push(item);
|
|
526
|
+
}
|
|
527
|
+
return sanitized;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function normalizeFreeformToolArguments(value) {
|
|
531
|
+
if (typeof value === 'string') return stringifyJsonValue({ input: value }, '{"input":""}');
|
|
532
|
+
if (value == null) return '{"input":""}';
|
|
533
|
+
if (isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'input')) {
|
|
534
|
+
return stringifyJsonValue(value, '{"input":""}');
|
|
535
|
+
}
|
|
536
|
+
return stringifyJsonValue({ input: normalizeResponsesToolOutput(value) }, '{"input":""}');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function toOpenAiToolCall(item, fallbackIndex) {
|
|
540
|
+
if (!isRecord(item)) return null;
|
|
541
|
+
const callId = asTrimmedString(item.call_id || item.id) || `call_${crypto.randomBytes(8).toString('hex')}_${fallbackIndex}`;
|
|
542
|
+
const name = asTrimmedString(item.name) || asTrimmedString(item.server_label);
|
|
543
|
+
if (!name) return null;
|
|
544
|
+
const type = asTrimmedString(item.type).toLowerCase();
|
|
545
|
+
const rawArguments = item.arguments != null ? item.arguments : item.input;
|
|
546
|
+
const args = type === 'custom_tool_call' && item.arguments == null
|
|
547
|
+
? normalizeFreeformToolArguments(rawArguments)
|
|
548
|
+
: normalizeOpenAiToolArguments(rawArguments);
|
|
549
|
+
return {
|
|
550
|
+
id: callId,
|
|
551
|
+
type: 'function',
|
|
552
|
+
function: {
|
|
553
|
+
name,
|
|
554
|
+
arguments: args
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function hasOpenAiMessageContent(content) {
|
|
560
|
+
return typeof content === 'string'
|
|
561
|
+
? content.trim().length > 0
|
|
562
|
+
: Array.isArray(content) && content.length > 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
381
565
|
function normalizeResponsesInputToChatMessages(input) {
|
|
382
|
-
// 参考
|
|
383
|
-
//
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
out.push({ type: 'text', text: block.refusal });
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
if (type === 'input_image') {
|
|
399
|
-
const raw = block.image_url != null ? block.image_url : block.imageUrl;
|
|
400
|
-
const url = typeof raw === 'string'
|
|
401
|
-
? raw
|
|
402
|
-
: (raw && typeof raw === 'object' && typeof raw.url === 'string' ? raw.url : '');
|
|
403
|
-
if (url) {
|
|
404
|
-
out.push({ type: 'image_url', image_url: { url } });
|
|
405
|
-
}
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (type === 'image_url' && block.image_url) {
|
|
409
|
-
out.push({ type: 'image_url', image_url: block.image_url });
|
|
410
|
-
}
|
|
566
|
+
// 参考 metapi 的 Responses → Chat 桥接:聚合连续 tool calls、丢弃孤儿 tool outputs,
|
|
567
|
+
// 并保留 reasoning / richer content blocks / developer-role compatibility。
|
|
568
|
+
const messages = [];
|
|
569
|
+
const normalizedInput = stripOrphanedResponsesToolOutputs(input);
|
|
570
|
+
let functionCallIndex = 0;
|
|
571
|
+
let pendingToolCalls = [];
|
|
572
|
+
const emittedToolCallIds = new Set();
|
|
573
|
+
|
|
574
|
+
const flushPendingToolCalls = () => {
|
|
575
|
+
if (pendingToolCalls.length <= 0) return;
|
|
576
|
+
for (const toolCall of pendingToolCalls) {
|
|
577
|
+
const callId = asTrimmedString(toolCall.id);
|
|
578
|
+
if (callId) emittedToolCallIds.add(callId);
|
|
411
579
|
}
|
|
412
|
-
|
|
413
|
-
|
|
580
|
+
messages.push({
|
|
581
|
+
role: 'assistant',
|
|
582
|
+
content: null,
|
|
583
|
+
tool_calls: pendingToolCalls
|
|
584
|
+
});
|
|
585
|
+
pendingToolCalls = [];
|
|
414
586
|
};
|
|
415
587
|
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}]
|
|
433
|
-
};
|
|
588
|
+
const pushToolOutputMessage = (callIdRaw, outputRaw) => {
|
|
589
|
+
const toolCallId = asTrimmedString(callIdRaw);
|
|
590
|
+
if (!toolCallId) return;
|
|
591
|
+
messages.push({
|
|
592
|
+
role: 'tool',
|
|
593
|
+
tool_call_id: toolCallId,
|
|
594
|
+
content: normalizeResponsesToolOutput(outputRaw)
|
|
595
|
+
});
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const processInputItem = (item) => {
|
|
599
|
+
if (typeof item === 'string') {
|
|
600
|
+
flushPendingToolCalls();
|
|
601
|
+
const text = item.trim();
|
|
602
|
+
if (text) messages.push({ role: 'user', content: text });
|
|
603
|
+
return;
|
|
434
604
|
}
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
605
|
+
if (!isRecord(item)) return;
|
|
606
|
+
|
|
607
|
+
const itemType = asTrimmedString(item.type).toLowerCase();
|
|
608
|
+
if (itemType === 'function_call' || itemType === 'custom_tool_call') {
|
|
609
|
+
const toolCall = toOpenAiToolCall(item, functionCallIndex);
|
|
610
|
+
functionCallIndex += 1;
|
|
611
|
+
if (toolCall) pendingToolCalls.push(toolCall);
|
|
612
|
+
return;
|
|
442
613
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
614
|
+
|
|
615
|
+
if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
|
|
616
|
+
flushPendingToolCalls();
|
|
617
|
+
const toolCallId = asTrimmedString(item.call_id || item.id);
|
|
618
|
+
if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
|
|
619
|
+
pushToolOutputMessage(toolCallId, item.output != null ? item.output : item.content);
|
|
620
|
+
return;
|
|
449
621
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
622
|
+
|
|
623
|
+
if (itemType === 'reasoning') {
|
|
624
|
+
// Any non-tool-call item is a sequence boundary: keep only consecutive
|
|
625
|
+
// tool calls in the same assistant `tool_calls` message.
|
|
626
|
+
flushPendingToolCalls();
|
|
627
|
+
const reasoningContent = toOpenAiMessageContent(item.summary != null ? item.summary : (item.content != null ? item.content : item));
|
|
628
|
+
const reasoningSignature = asTrimmedString(item.encrypted_content || item.reasoning_signature);
|
|
629
|
+
if (!hasOpenAiMessageContent(reasoningContent) && !reasoningSignature) return;
|
|
630
|
+
const message = { role: 'assistant', content: reasoningContent };
|
|
631
|
+
if (reasoningSignature) message.reasoning_signature = reasoningSignature;
|
|
632
|
+
messages.push(message);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
flushPendingToolCalls();
|
|
637
|
+
const role = asTrimmedString(item.role).toLowerCase() || 'user';
|
|
638
|
+
const normalizedRole = role === 'developer' ? 'system' : role;
|
|
639
|
+
const content = toOpenAiMessageContent(item.content != null ? item.content : (item.input != null ? item.input : item));
|
|
640
|
+
|
|
641
|
+
if (normalizedRole === 'tool') {
|
|
642
|
+
const toolCallId = asTrimmedString(item.tool_call_id || item.call_id || item.id);
|
|
643
|
+
if (!toolCallId || !emittedToolCallIds.has(toolCallId)) return;
|
|
644
|
+
pushToolOutputMessage(toolCallId, item.content);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (!hasOpenAiMessageContent(content)) return;
|
|
649
|
+
const message = { role: normalizedRole, content };
|
|
650
|
+
const phase = asTrimmedString(item.phase);
|
|
651
|
+
if (phase) message.phase = phase;
|
|
652
|
+
messages.push(message);
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
if (typeof normalizedInput === 'string') {
|
|
656
|
+
const text = normalizedInput.trim();
|
|
657
|
+
if (text) messages.push({ role: 'user', content: text });
|
|
658
|
+
} else if (Array.isArray(normalizedInput)) {
|
|
659
|
+
for (const item of normalizedInput) processInputItem(item);
|
|
660
|
+
} else if (isRecord(normalizedInput)) {
|
|
661
|
+
processInputItem(normalizedInput);
|
|
662
|
+
}
|
|
663
|
+
flushPendingToolCalls();
|
|
664
|
+
return messages;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function normalizeFunctionToolForChat(tool) {
|
|
668
|
+
if (!isRecord(tool)) return null;
|
|
669
|
+
const sourceFn = isRecord(tool.function) ? tool.function : tool;
|
|
670
|
+
const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
|
|
671
|
+
if (!name) return null;
|
|
672
|
+
const fn = { name };
|
|
673
|
+
const description = asTrimmedString(sourceFn.description) || asTrimmedString(tool.description);
|
|
674
|
+
if (description) fn.description = description;
|
|
675
|
+
if (sourceFn.parameters !== undefined) {
|
|
676
|
+
fn.parameters = cloneJsonValue(sourceFn.parameters);
|
|
677
|
+
} else if (tool.parameters !== undefined) {
|
|
678
|
+
fn.parameters = cloneJsonValue(tool.parameters);
|
|
679
|
+
}
|
|
680
|
+
if (typeof sourceFn.strict === 'boolean') {
|
|
681
|
+
fn.strict = sourceFn.strict;
|
|
682
|
+
} else if (typeof tool.strict === 'boolean') {
|
|
683
|
+
fn.strict = tool.strict;
|
|
684
|
+
}
|
|
685
|
+
return { type: 'function', function: fn };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function buildLocalShellToolForChat(tool) {
|
|
689
|
+
return {
|
|
690
|
+
type: 'function',
|
|
691
|
+
function: {
|
|
692
|
+
name: asTrimmedString(tool && tool.name) || 'local_shell',
|
|
693
|
+
description: asTrimmedString(tool && tool.description) || 'Run a local shell command and return its output.',
|
|
694
|
+
parameters: {
|
|
695
|
+
type: 'object',
|
|
696
|
+
properties: {
|
|
697
|
+
cmd: { type: 'string', description: 'Shell command to execute.' },
|
|
698
|
+
yield_time_ms: { type: 'number', description: 'Milliseconds to wait before yielding partial output.' },
|
|
699
|
+
max_output_tokens: { type: 'number', description: 'Maximum output tokens to return.' }
|
|
700
|
+
},
|
|
701
|
+
required: ['cmd'],
|
|
702
|
+
additionalProperties: true
|
|
703
|
+
}
|
|
453
704
|
}
|
|
454
|
-
return null;
|
|
455
705
|
};
|
|
706
|
+
}
|
|
456
707
|
|
|
457
|
-
|
|
458
|
-
|
|
708
|
+
function buildFreeformToolForChat(tool, fallbackName = 'custom_tool') {
|
|
709
|
+
return {
|
|
710
|
+
type: 'function',
|
|
711
|
+
function: {
|
|
712
|
+
name: asTrimmedString(tool && tool.name) || fallbackName,
|
|
713
|
+
description: asTrimmedString(tool && tool.description) || 'Pass raw freeform input to the local tool.',
|
|
714
|
+
parameters: {
|
|
715
|
+
type: 'object',
|
|
716
|
+
properties: {
|
|
717
|
+
input: { type: 'string', description: 'Raw tool input.' }
|
|
718
|
+
},
|
|
719
|
+
required: ['input'],
|
|
720
|
+
additionalProperties: false
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const MAX_RESPONSES_TOOL_NAMESPACE_DEPTH = 5;
|
|
727
|
+
|
|
728
|
+
function rememberResponsesToolType(tool, target, depth = 0) {
|
|
729
|
+
if (!isRecord(tool) || !target || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return;
|
|
730
|
+
const type = asTrimmedString(tool.type).toLowerCase();
|
|
731
|
+
if (type === 'namespace' && Array.isArray(tool.tools)) {
|
|
732
|
+
for (const inner of tool.tools) rememberResponsesToolType(inner, target, depth + 1);
|
|
733
|
+
return;
|
|
459
734
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
735
|
+
const sourceFn = isRecord(tool.function) ? tool.function : tool;
|
|
736
|
+
const name = asTrimmedString(sourceFn.name) || asTrimmedString(tool.name);
|
|
737
|
+
if (!name) return;
|
|
738
|
+
if (type === 'local_shell') {
|
|
739
|
+
target[name] = 'local_shell_call';
|
|
740
|
+
return;
|
|
463
741
|
}
|
|
464
|
-
if (!
|
|
465
|
-
|
|
742
|
+
if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
|
|
743
|
+
target[name] = 'custom_tool_call';
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (type === 'function') {
|
|
747
|
+
target[name] = 'function_call';
|
|
466
748
|
}
|
|
749
|
+
}
|
|
467
750
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
751
|
+
function collectResponsesToolTypesByName(tools) {
|
|
752
|
+
const result = {};
|
|
753
|
+
if (!Array.isArray(tools)) return result;
|
|
754
|
+
for (const tool of tools) rememberResponsesToolType(tool, result);
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function extractFreeformInputFromChatArguments(argumentsText) {
|
|
759
|
+
if (typeof argumentsText !== 'string') return '';
|
|
760
|
+
const parsed = parseJsonValueOrNull(argumentsText);
|
|
761
|
+
if (isRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, 'input')) {
|
|
762
|
+
return typeof parsed.input === 'string' ? parsed.input : normalizeResponsesToolOutput(parsed.input);
|
|
763
|
+
}
|
|
764
|
+
return argumentsText;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function extractLocalShellActionFromChatArguments(argumentsText) {
|
|
768
|
+
const parsed = parseJsonValueOrNull(argumentsText);
|
|
769
|
+
if (isRecord(parsed)) return cloneJsonValue(parsed);
|
|
770
|
+
return { cmd: typeof argumentsText === 'string' ? argumentsText : '' };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function buildResponsesToolCallItemFromChatToolCall(toolCall, toolTypesByName = {}) {
|
|
774
|
+
if (!isRecord(toolCall)) return null;
|
|
775
|
+
const fn = isRecord(toolCall.function) ? toolCall.function : {};
|
|
776
|
+
const name = asTrimmedString(fn.name);
|
|
777
|
+
if (!name) return null;
|
|
778
|
+
const callId = asTrimmedString(toolCall.id) || `call_${crypto.randomBytes(8).toString('hex')}`;
|
|
779
|
+
const argumentsText = typeof fn.arguments === 'string' ? fn.arguments : '';
|
|
780
|
+
const responseType = toolTypesByName && toolTypesByName[name] ? toolTypesByName[name] : 'function_call';
|
|
781
|
+
if (responseType === 'custom_tool_call') {
|
|
782
|
+
return {
|
|
783
|
+
type: 'custom_tool_call',
|
|
784
|
+
call_id: callId,
|
|
785
|
+
name,
|
|
786
|
+
input: extractFreeformInputFromChatArguments(argumentsText)
|
|
787
|
+
};
|
|
472
788
|
}
|
|
473
|
-
if (
|
|
474
|
-
return
|
|
789
|
+
if (responseType === 'local_shell_call') {
|
|
790
|
+
return {
|
|
791
|
+
type: 'local_shell_call',
|
|
792
|
+
call_id: callId,
|
|
793
|
+
name,
|
|
794
|
+
action: extractLocalShellActionFromChatArguments(argumentsText)
|
|
795
|
+
};
|
|
475
796
|
}
|
|
797
|
+
return {
|
|
798
|
+
type: 'function_call',
|
|
799
|
+
call_id: callId,
|
|
800
|
+
name,
|
|
801
|
+
arguments: argumentsText
|
|
802
|
+
};
|
|
803
|
+
}
|
|
476
804
|
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
805
|
+
function normalizeSingleResponsesToolToChatTools(tool, depth = 0) {
|
|
806
|
+
if (!isRecord(tool) || depth > MAX_RESPONSES_TOOL_NAMESPACE_DEPTH) return [];
|
|
807
|
+
const type = asTrimmedString(tool.type).toLowerCase();
|
|
808
|
+
if (type === 'namespace' && Array.isArray(tool.tools)) {
|
|
809
|
+
return tool.tools.flatMap((inner) => normalizeSingleResponsesToolToChatTools(inner, depth + 1));
|
|
810
|
+
}
|
|
811
|
+
if (type === 'function') {
|
|
812
|
+
const converted = normalizeFunctionToolForChat(tool);
|
|
813
|
+
return converted ? [converted] : [];
|
|
814
|
+
}
|
|
815
|
+
if (type === 'local_shell') {
|
|
816
|
+
return [buildLocalShellToolForChat(tool)];
|
|
817
|
+
}
|
|
818
|
+
const name = asTrimmedString(tool.name);
|
|
819
|
+
if (type === 'custom' || type === 'custom_tool' || (!type && name === 'apply_patch')) {
|
|
820
|
+
return [buildFreeformToolForChat(tool, name || 'custom_tool')];
|
|
480
821
|
}
|
|
822
|
+
// Hosted Responses tools such as web_search/image_generation/computer_use
|
|
823
|
+
// do not have a safe Chat Completions representation. Passing them through
|
|
824
|
+
// as-is makes OpenAI-compatible chat gateways reject the request, so drop
|
|
825
|
+
// them instead of pretending the shapes are compatible.
|
|
481
826
|
return [];
|
|
482
827
|
}
|
|
483
828
|
|
|
484
829
|
function normalizeResponsesToolsToChatTools(tools) {
|
|
485
830
|
if (!Array.isArray(tools)) return tools;
|
|
486
|
-
return tools
|
|
487
|
-
.map((tool) => {
|
|
488
|
-
if (!tool || typeof tool !== 'object') return null;
|
|
489
|
-
if (tool.type !== 'function') return tool;
|
|
490
|
-
const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
|
|
491
|
-
? tool.function
|
|
492
|
-
: {};
|
|
493
|
-
const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
|
|
494
|
-
? sourceFn.name.trim()
|
|
495
|
-
: (typeof tool.name === 'string' ? tool.name.trim() : '');
|
|
496
|
-
if (!name) return null;
|
|
497
|
-
const description = typeof sourceFn.description === 'string'
|
|
498
|
-
? sourceFn.description
|
|
499
|
-
: (typeof tool.description === 'string' ? tool.description : undefined);
|
|
500
|
-
const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
|
|
501
|
-
? sourceFn.parameters
|
|
502
|
-
: (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : {});
|
|
503
|
-
const strict = typeof sourceFn.strict === 'boolean'
|
|
504
|
-
? sourceFn.strict
|
|
505
|
-
: (typeof tool.strict === 'boolean' ? tool.strict : undefined);
|
|
506
|
-
const fn = { name, parameters };
|
|
507
|
-
if (description !== undefined) fn.description = description;
|
|
508
|
-
if (strict !== undefined) fn.strict = strict;
|
|
509
|
-
return { type: 'function', function: fn };
|
|
510
|
-
})
|
|
511
|
-
.filter(Boolean);
|
|
831
|
+
return tools.flatMap((tool) => normalizeSingleResponsesToolToChatTools(tool));
|
|
512
832
|
}
|
|
513
833
|
|
|
514
834
|
function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
|
|
515
|
-
if (
|
|
516
|
-
if (
|
|
517
|
-
|
|
835
|
+
if (toolChoice === undefined) return undefined;
|
|
836
|
+
if (typeof toolChoice === 'string') return toolChoice;
|
|
837
|
+
if (!isRecord(toolChoice)) return toolChoice;
|
|
838
|
+
|
|
839
|
+
const type = asTrimmedString(toolChoice.type).toLowerCase();
|
|
840
|
+
if (type === 'tool' || type === 'function' || type === 'custom' || type === 'custom_tool' || type === 'local_shell') {
|
|
841
|
+
if (isRecord(toolChoice.function) && asTrimmedString(toolChoice.function.name)) return cloneJsonValue(toolChoice);
|
|
842
|
+
const name = asTrimmedString(toolChoice.name) || asTrimmedString(toolChoice.server_label);
|
|
843
|
+
if (!name) return 'required';
|
|
844
|
+
return { type: 'function', function: { name } };
|
|
845
|
+
}
|
|
846
|
+
if (type === 'auto' || type === 'none' || type === 'required') return type;
|
|
847
|
+
return 'auto';
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function getChatToolChoiceName(toolChoice) {
|
|
851
|
+
if (!isRecord(toolChoice)) return '';
|
|
852
|
+
if (isRecord(toolChoice.function)) return asTrimmedString(toolChoice.function.name);
|
|
853
|
+
return '';
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function pruneInvalidChatToolChoice(chatBody) {
|
|
857
|
+
if (!isRecord(chatBody) || !Array.isArray(chatBody.tools)) return;
|
|
858
|
+
if (chatBody.tools.length === 0) {
|
|
859
|
+
delete chatBody.tools;
|
|
860
|
+
delete chatBody.tool_choice;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const chosenName = getChatToolChoiceName(chatBody.tool_choice);
|
|
864
|
+
if (!chosenName) return;
|
|
865
|
+
const toolNames = new Set(chatBody.tools
|
|
866
|
+
.map((tool) => isRecord(tool) && isRecord(tool.function) ? asTrimmedString(tool.function.name) : '')
|
|
867
|
+
.filter(Boolean));
|
|
868
|
+
if (!toolNames.has(chosenName)) {
|
|
869
|
+
delete chatBody.tool_choice;
|
|
518
870
|
}
|
|
519
|
-
return toolChoice;
|
|
520
871
|
}
|
|
521
872
|
|
|
522
873
|
function buildChatCompletionsBodyFromResponsesPayload(payload) {
|
|
523
|
-
const source =
|
|
874
|
+
const source = isRecord(payload) ? payload : {};
|
|
524
875
|
const messages = normalizeResponsesInputToChatMessages(source.input);
|
|
525
|
-
const instructions =
|
|
876
|
+
const instructions = asTrimmedString(source.instructions);
|
|
526
877
|
if (instructions) {
|
|
527
878
|
messages.unshift({ role: 'system', content: instructions });
|
|
528
879
|
}
|
|
@@ -536,12 +887,12 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
536
887
|
const passthroughKeys = [
|
|
537
888
|
'frequency_penalty',
|
|
538
889
|
'presence_penalty',
|
|
539
|
-
'response_format',
|
|
540
890
|
'stop',
|
|
541
891
|
'temperature',
|
|
542
892
|
'top_p',
|
|
543
893
|
'tools',
|
|
544
894
|
'tool_choice',
|
|
895
|
+
'parallel_tool_calls',
|
|
545
896
|
'logprobs',
|
|
546
897
|
'top_logprobs',
|
|
547
898
|
'kbs',
|
|
@@ -551,7 +902,9 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
551
902
|
'n',
|
|
552
903
|
'modalities',
|
|
553
904
|
'audio',
|
|
554
|
-
'
|
|
905
|
+
'reasoning',
|
|
906
|
+
'reasoning_effort',
|
|
907
|
+
'service_tier'
|
|
555
908
|
];
|
|
556
909
|
for (const key of passthroughKeys) {
|
|
557
910
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
@@ -560,11 +913,22 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
560
913
|
} else if (key === 'tool_choice') {
|
|
561
914
|
chatBody[key] = normalizeResponsesToolChoiceToChatToolChoice(source[key]);
|
|
562
915
|
} else {
|
|
563
|
-
chatBody[key] = source[key];
|
|
916
|
+
chatBody[key] = cloneJsonValue(source[key]);
|
|
564
917
|
}
|
|
565
918
|
}
|
|
566
919
|
}
|
|
567
920
|
|
|
921
|
+
if (Object.prototype.hasOwnProperty.call(source, 'response_format')) {
|
|
922
|
+
chatBody.response_format = cloneJsonValue(source.response_format);
|
|
923
|
+
} else if (isRecord(source.text) && source.text.format !== undefined) {
|
|
924
|
+
chatBody.response_format = cloneJsonValue(source.text.format);
|
|
925
|
+
}
|
|
926
|
+
if (isRecord(source.text) && asTrimmedString(source.text.verbosity)) {
|
|
927
|
+
chatBody.verbosity = asTrimmedString(source.text.verbosity);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
pruneInvalidChatToolChoice(chatBody);
|
|
931
|
+
|
|
568
932
|
if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
|
|
569
933
|
chatBody.max_tokens = source.max_tokens;
|
|
570
934
|
} else if (source.max_output_tokens != null) {
|
|
@@ -712,6 +1076,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
712
1076
|
content: [{ type: 'output_text', text: '' }]
|
|
713
1077
|
};
|
|
714
1078
|
state.output.push(state.messageItem);
|
|
1079
|
+
state.outputStarted = true;
|
|
1080
|
+
beginChatStreamResponsesSse(state);
|
|
715
1081
|
writeSse(state.res, 'response.output_item.added', {
|
|
716
1082
|
type: 'response.output_item.added',
|
|
717
1083
|
output_index: state.output.length - 1,
|
|
@@ -764,6 +1130,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
764
1130
|
|
|
765
1131
|
function finishChatStreamResponsesSse(state) {
|
|
766
1132
|
if (state.finished) return;
|
|
1133
|
+
beginChatStreamResponsesSse(state);
|
|
767
1134
|
state.finished = true;
|
|
768
1135
|
stopChatStreamHeartbeat(state);
|
|
769
1136
|
|
|
@@ -787,14 +1154,11 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
787
1154
|
|
|
788
1155
|
for (const toolCall of state.toolCalls) {
|
|
789
1156
|
if (!toolCall) continue;
|
|
790
|
-
const item = {
|
|
791
|
-
|
|
792
|
-
call_id: toolCall.id || `call_${crypto.randomBytes(8).toString('hex')}`,
|
|
793
|
-
name: toolCall.function && typeof toolCall.function.name === 'string' ? toolCall.function.name : '',
|
|
794
|
-
arguments: toolCall.function && typeof toolCall.function.arguments === 'string' ? toolCall.function.arguments : ''
|
|
795
|
-
};
|
|
1157
|
+
const item = buildResponsesToolCallItemFromChatToolCall(toolCall, state.toolTypesByName || {});
|
|
1158
|
+
if (!item) continue;
|
|
796
1159
|
const outputIndex = state.output.length;
|
|
797
1160
|
state.output.push(item);
|
|
1161
|
+
state.outputStarted = true;
|
|
798
1162
|
writeSse(state.res, 'response.output_item.added', {
|
|
799
1163
|
type: 'response.output_item.added',
|
|
800
1164
|
output_index: outputIndex,
|
|
@@ -829,13 +1193,70 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
829
1193
|
} catch (_) {}
|
|
830
1194
|
}
|
|
831
1195
|
|
|
1196
|
+
function beginChatStreamResponsesSse(state) {
|
|
1197
|
+
if (!state || state.started) return;
|
|
1198
|
+
state.started = true;
|
|
1199
|
+
const res = state.res;
|
|
1200
|
+
if (!res.headersSent) {
|
|
1201
|
+
res.writeHead(200, {
|
|
1202
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
1203
|
+
'Cache-Control': 'no-cache',
|
|
1204
|
+
'Connection': 'keep-alive',
|
|
1205
|
+
'X-Accel-Buffering': 'no'
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
startChatStreamHeartbeat(state);
|
|
1209
|
+
if (typeof res.on === 'function' && !state.closeListenerAttached) {
|
|
1210
|
+
state.closeListenerAttached = true;
|
|
1211
|
+
res.on('close', () => {
|
|
1212
|
+
stopChatStreamHeartbeat(state);
|
|
1213
|
+
if (!state.finished && state.upstreamReq) {
|
|
1214
|
+
try { state.upstreamReq.destroy(new Error('client aborted')); } catch (_) {}
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
writeSse(res, 'response.created', {
|
|
1219
|
+
type: 'response.created',
|
|
1220
|
+
response: {
|
|
1221
|
+
id: state.responseId,
|
|
1222
|
+
model: state.model,
|
|
1223
|
+
created_at: state.createdAt
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
832
1228
|
function failChatStreamResponsesSse(state, message) {
|
|
833
1229
|
if (!state || state.finished) return;
|
|
1230
|
+
beginChatStreamResponsesSse(state);
|
|
834
1231
|
state.finished = true;
|
|
835
1232
|
stopChatStreamHeartbeat(state);
|
|
836
1233
|
failResponsesSseRaw(state.res, message);
|
|
837
1234
|
}
|
|
838
1235
|
|
|
1236
|
+
function createChatStreamResponsesSseState(res, model, options = {}) {
|
|
1237
|
+
let sequence = 0;
|
|
1238
|
+
return {
|
|
1239
|
+
res,
|
|
1240
|
+
upstreamReq: null,
|
|
1241
|
+
responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
|
|
1242
|
+
model: typeof model === 'string' ? model : '',
|
|
1243
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
1244
|
+
output: [],
|
|
1245
|
+
messageItem: null,
|
|
1246
|
+
messageText: '',
|
|
1247
|
+
toolCalls: [],
|
|
1248
|
+
toolTypesByName: options.toolTypesByName || {},
|
|
1249
|
+
finished: false,
|
|
1250
|
+
started: false,
|
|
1251
|
+
outputStarted: false,
|
|
1252
|
+
closeListenerAttached: false,
|
|
1253
|
+
nextSeq: () => {
|
|
1254
|
+
sequence += 1;
|
|
1255
|
+
return sequence;
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
839
1260
|
function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
|
|
840
1261
|
const parsed = new URL(targetUrl);
|
|
841
1262
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
@@ -853,9 +1274,13 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
853
1274
|
: 30000;
|
|
854
1275
|
const res = options.res;
|
|
855
1276
|
const model = typeof options.model === 'string' ? options.model : '';
|
|
1277
|
+
const sharedState = options.streamState || createChatStreamResponsesSseState(res, model, {
|
|
1278
|
+
toolTypesByName: options.toolTypesByName || {}
|
|
1279
|
+
});
|
|
856
1280
|
|
|
857
1281
|
return new Promise((resolve) => {
|
|
858
1282
|
let settled = false;
|
|
1283
|
+
let streamAccepted = false;
|
|
859
1284
|
const finish = (value) => {
|
|
860
1285
|
if (settled) return;
|
|
861
1286
|
settled = true;
|
|
@@ -873,13 +1298,21 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
873
1298
|
const status = upstreamRes.statusCode || 0;
|
|
874
1299
|
const chunks = [];
|
|
875
1300
|
const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
|
|
1301
|
+
streamAccepted = status >= 200 && status < 300 && /text\/event-stream/i.test(contentType);
|
|
1302
|
+
if (streamAccepted) {
|
|
1303
|
+
req.setTimeout(0);
|
|
1304
|
+
}
|
|
876
1305
|
let streamState = null;
|
|
877
1306
|
|
|
878
1307
|
const handleAbort = (reason) => {
|
|
879
1308
|
if (settled) return;
|
|
880
1309
|
if (streamState) {
|
|
881
|
-
|
|
882
|
-
|
|
1310
|
+
if (streamState.outputStarted) {
|
|
1311
|
+
failChatStreamResponsesSse(streamState, reason);
|
|
1312
|
+
finish({ ok: true });
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
finish({ ok: false, retryTransient: true, error: reason || 'upstream stream failed' });
|
|
883
1316
|
return;
|
|
884
1317
|
}
|
|
885
1318
|
if (res.headersSent) {
|
|
@@ -887,11 +1320,14 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
887
1320
|
finish({ ok: true });
|
|
888
1321
|
return;
|
|
889
1322
|
}
|
|
1323
|
+
const bodyText = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
1324
|
+
const transient = isTransientNetworkError(reason) || /aborted|stream aborted/i.test(String(reason || ''));
|
|
890
1325
|
finish({
|
|
891
1326
|
ok: false,
|
|
892
|
-
status,
|
|
1327
|
+
...(transient ? {} : { status }),
|
|
1328
|
+
...(transient ? { retryTransient: true } : {}),
|
|
893
1329
|
error: reason,
|
|
894
|
-
bodyText
|
|
1330
|
+
bodyText
|
|
895
1331
|
});
|
|
896
1332
|
};
|
|
897
1333
|
upstreamRes.on('error', (err) => handleAbort(err && err.message ? err.message : 'upstream stream failed'));
|
|
@@ -909,18 +1345,17 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
909
1345
|
return;
|
|
910
1346
|
}
|
|
911
1347
|
|
|
912
|
-
res.writeHead(200, {
|
|
913
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
914
|
-
'Cache-Control': 'no-cache',
|
|
915
|
-
'Connection': 'keep-alive',
|
|
916
|
-
'X-Accel-Buffering': 'no'
|
|
917
|
-
});
|
|
918
|
-
|
|
919
1348
|
if (!/text\/event-stream/i.test(contentType)) {
|
|
920
1349
|
upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
|
|
921
1350
|
upstreamRes.on('end', () => {
|
|
922
1351
|
const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
923
1352
|
const parsedJson = parseJsonOrError(text);
|
|
1353
|
+
res.writeHead(200, {
|
|
1354
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
1355
|
+
'Cache-Control': 'no-cache',
|
|
1356
|
+
'Connection': 'keep-alive',
|
|
1357
|
+
'X-Accel-Buffering': 'no'
|
|
1358
|
+
});
|
|
924
1359
|
if (parsedJson.error) {
|
|
925
1360
|
writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
|
|
926
1361
|
writeSse(res, 'done', '[DONE]');
|
|
@@ -928,42 +1363,20 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
928
1363
|
finish({ ok: true });
|
|
929
1364
|
return;
|
|
930
1365
|
}
|
|
931
|
-
sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model
|
|
1366
|
+
sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model, {
|
|
1367
|
+
toolTypesByName: options.toolTypesByName || {}
|
|
1368
|
+
}));
|
|
932
1369
|
res.end();
|
|
933
1370
|
finish({ ok: true });
|
|
934
1371
|
});
|
|
935
1372
|
return;
|
|
936
1373
|
}
|
|
937
1374
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
|
|
942
|
-
model,
|
|
943
|
-
createdAt: Math.floor(Date.now() / 1000),
|
|
944
|
-
output: [],
|
|
945
|
-
messageItem: null,
|
|
946
|
-
messageText: '',
|
|
947
|
-
toolCalls: [],
|
|
948
|
-
finished: false,
|
|
949
|
-
nextSeq: () => {
|
|
950
|
-
sequence += 1;
|
|
951
|
-
return sequence;
|
|
952
|
-
}
|
|
953
|
-
};
|
|
1375
|
+
const state = sharedState;
|
|
1376
|
+
state.upstreamReq = req;
|
|
1377
|
+
if (!state.model && model) state.model = model;
|
|
954
1378
|
streamState = state;
|
|
955
|
-
|
|
956
|
-
if (typeof res.on === 'function') {
|
|
957
|
-
res.on('close', () => stopChatStreamHeartbeat(state));
|
|
958
|
-
}
|
|
959
|
-
writeSse(res, 'response.created', {
|
|
960
|
-
type: 'response.created',
|
|
961
|
-
response: {
|
|
962
|
-
id: state.responseId,
|
|
963
|
-
model: state.model,
|
|
964
|
-
created_at: state.createdAt
|
|
965
|
-
}
|
|
966
|
-
});
|
|
1379
|
+
beginChatStreamResponsesSse(state);
|
|
967
1380
|
|
|
968
1381
|
let buffer = '';
|
|
969
1382
|
const handleEventBlock = (block) => {
|
|
@@ -981,6 +1394,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
981
1394
|
}
|
|
982
1395
|
const parsedChunk = parseJsonOrError(data);
|
|
983
1396
|
if (!parsedChunk.error) {
|
|
1397
|
+
beginChatStreamResponsesSse(state);
|
|
984
1398
|
writeChatCompletionChunkAsResponsesSse(state, parsedChunk.value);
|
|
985
1399
|
}
|
|
986
1400
|
};
|
|
@@ -1003,6 +1417,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
1003
1417
|
});
|
|
1004
1418
|
});
|
|
1005
1419
|
req.setTimeout(timeoutMs, () => {
|
|
1420
|
+
if (streamAccepted) return;
|
|
1006
1421
|
try { req.destroy(new Error('timeout')); } catch (_) {}
|
|
1007
1422
|
finish({ ok: false, error: 'timeout' });
|
|
1008
1423
|
});
|
|
@@ -1018,8 +1433,11 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
1018
1433
|
return { ok: false, error: 'failed to build upstream URL' };
|
|
1019
1434
|
}
|
|
1020
1435
|
let lastResult = null;
|
|
1436
|
+
const streamState = options.streamState || createChatStreamResponsesSseState(options.res, options.model, {
|
|
1437
|
+
toolTypesByName: options.toolTypesByName || {}
|
|
1438
|
+
});
|
|
1021
1439
|
for (const url of urls) {
|
|
1022
|
-
const result = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(url, options));
|
|
1440
|
+
const result = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(url, { ...options, streamState }));
|
|
1023
1441
|
lastResult = result;
|
|
1024
1442
|
if (result && result.retry) continue;
|
|
1025
1443
|
return result;
|
|
@@ -1303,7 +1721,6 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
1303
1721
|
function createBuiltinProxyServer(settings, upstream) {
|
|
1304
1722
|
const connections = new Set();
|
|
1305
1723
|
const timeoutMs = settings.timeoutMs;
|
|
1306
|
-
|
|
1307
1724
|
const server = http.createServer((req, res) => {
|
|
1308
1725
|
let parsedIncoming;
|
|
1309
1726
|
try {
|
|
@@ -1376,8 +1793,8 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
1376
1793
|
|
|
1377
1794
|
// Responses shim:
|
|
1378
1795
|
// - Codex CLI 默认走 /v1/responses(含 SSE)
|
|
1379
|
-
// -
|
|
1380
|
-
//
|
|
1796
|
+
// - SSE/streaming 任务优先走 chat/completions fallback,避免卡在会接收但不产出 Responses 的兼容网关
|
|
1797
|
+
// - 非流式请求仍优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
|
|
1381
1798
|
if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
|
|
1382
1799
|
void (async () => {
|
|
1383
1800
|
const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
|
|
@@ -1401,6 +1818,34 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
1401
1818
|
'X-Codexmate-Proxy': '1'
|
|
1402
1819
|
};
|
|
1403
1820
|
|
|
1821
|
+
const model = typeof payload.model === 'string' ? payload.model : '';
|
|
1822
|
+
const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
|
|
1823
|
+
const toolTypesByName = collectResponsesToolTypesByName(payload.tools);
|
|
1824
|
+
|
|
1825
|
+
if (wantsStream) {
|
|
1826
|
+
const streamingChatBody = { ...chatBody, stream: true };
|
|
1827
|
+
const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
|
|
1828
|
+
method: 'POST',
|
|
1829
|
+
headers: commonHeaders,
|
|
1830
|
+
timeoutMs,
|
|
1831
|
+
body: streamingChatBody,
|
|
1832
|
+
res,
|
|
1833
|
+
model,
|
|
1834
|
+
toolTypesByName
|
|
1835
|
+
});
|
|
1836
|
+
if (!streamed.ok) {
|
|
1837
|
+
if (!res.headersSent) {
|
|
1838
|
+
res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1839
|
+
res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
|
|
1840
|
+
} else if (!res.writableEnded) {
|
|
1841
|
+
writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
|
|
1842
|
+
writeSse(res, 'done', '[DONE]');
|
|
1843
|
+
res.end();
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1404
1849
|
const upstreamResponses = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'responses', {
|
|
1405
1850
|
method: 'POST',
|
|
1406
1851
|
headers: commonHeaders,
|
|
@@ -1452,32 +1897,6 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
1452
1897
|
// Treat that as an unsupported Responses endpoint and try the chat fallback.
|
|
1453
1898
|
}
|
|
1454
1899
|
|
|
1455
|
-
const model = typeof payload.model === 'string' ? payload.model : '';
|
|
1456
|
-
const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
|
|
1457
|
-
|
|
1458
|
-
if (wantsStream) {
|
|
1459
|
-
const streamingChatBody = { ...chatBody, stream: true };
|
|
1460
|
-
const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
|
|
1461
|
-
method: 'POST',
|
|
1462
|
-
headers: commonHeaders,
|
|
1463
|
-
timeoutMs,
|
|
1464
|
-
body: streamingChatBody,
|
|
1465
|
-
res,
|
|
1466
|
-
model
|
|
1467
|
-
});
|
|
1468
|
-
if (!streamed.ok) {
|
|
1469
|
-
if (!res.headersSent) {
|
|
1470
|
-
res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1471
|
-
res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
|
|
1472
|
-
} else if (!res.writableEnded) {
|
|
1473
|
-
writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
|
|
1474
|
-
writeSse(res, 'done', '[DONE]');
|
|
1475
|
-
res.end();
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
1900
|
const upstreamChat = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
|
|
1482
1901
|
method: 'POST',
|
|
1483
1902
|
headers: commonHeaders,
|
|
@@ -1503,7 +1922,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
1503
1922
|
return;
|
|
1504
1923
|
}
|
|
1505
1924
|
|
|
1506
|
-
const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model);
|
|
1925
|
+
const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model, { toolTypesByName });
|
|
1507
1926
|
|
|
1508
1927
|
if (wantsStream) {
|
|
1509
1928
|
res.writeHead(200, {
|