codexmate 0.0.26 → 0.0.28
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 +7 -2
- package/README.zh.md +7 -2
- package/cli/builtin-proxy.js +636 -95
- package/cli/openai-bridge.js +497 -5
- package/cli.js +75 -29
- package/lib/cli-models-utils.js +71 -10
- package/package.json +3 -1
- package/plugins/prompt-templates/computed.mjs +1 -1
- package/plugins/prompt-templates/methods.mjs +0 -66
- package/plugins/prompt-templates/overview.mjs +1 -0
- package/web-ui/app.js +16 -16
- package/web-ui/logic.codex.mjs +56 -0
- package/web-ui/logic.sessions.mjs +56 -0
- package/web-ui/modules/app.computed.dashboard.mjs +54 -0
- package/web-ui/modules/app.computed.session.mjs +48 -0
- package/web-ui/modules/app.methods.claude-config.mjs +18 -7
- package/web-ui/modules/app.methods.codex-config.mjs +35 -3
- package/web-ui/modules/app.methods.providers.mjs +9 -1
- package/web-ui/modules/app.methods.session-actions.mjs +2 -5
- package/web-ui/modules/app.methods.session-browser.mjs +4 -5
- package/web-ui/modules/app.methods.session-trash.mjs +19 -4
- package/web-ui/modules/app.methods.startup-claude.mjs +12 -1
- package/web-ui/modules/i18n.dict.mjs +28 -32
- package/web-ui/modules/provider-url-display.mjs +17 -0
- package/web-ui/partials/index/panel-config-claude.html +5 -1
- package/web-ui/partials/index/panel-config-codex.html +33 -4
- package/web-ui/partials/index/panel-plugins.html +3 -29
- package/web-ui/partials/index/panel-sessions.html +0 -10
- package/web-ui/partials/index/panel-settings.html +62 -67
- package/web-ui/partials/index/panel-usage.html +31 -2
- package/web-ui/session-helpers.mjs +2 -2
- package/web-ui/styles/base-theme.css +47 -34
- package/web-ui/styles/controls-forms.css +27 -28
- package/web-ui/styles/layout-shell.css +37 -34
- package/web-ui/styles/modals-core.css +12 -10
- package/web-ui/styles/navigation-panels.css +36 -35
- package/web-ui/styles/responsive.css +4 -4
- package/web-ui/styles/sessions-list.css +10 -6
- package/web-ui/styles/sessions-usage.css +95 -0
- package/web-ui/styles/settings-panel.css +19 -0
- package/web-ui/styles/titles-cards.css +90 -26
package/cli/builtin-proxy.js
CHANGED
|
@@ -118,6 +118,15 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
118
118
|
return false;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function shouldFallbackFromUpstreamResponsesFailure(error) {
|
|
122
|
+
const text = String(error || '').trim();
|
|
123
|
+
if (!text) return false;
|
|
124
|
+
if (/timeout/i.test(text)) return true;
|
|
125
|
+
if (/socket hang up/i.test(text)) return true;
|
|
126
|
+
if (/ECONNRESET/i.test(text)) return true;
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
function proxyRequestJson(targetUrl, options = {}) {
|
|
122
131
|
const parsed = new URL(targetUrl);
|
|
123
132
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
@@ -174,70 +183,182 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
174
183
|
});
|
|
175
184
|
}
|
|
176
185
|
|
|
177
|
-
function
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (typeof item === 'string') return item;
|
|
190
|
-
if (typeof item === 'object') {
|
|
191
|
-
if (typeof item.text === 'string') return item.text;
|
|
192
|
-
if (typeof item.content === 'string') return item.content;
|
|
193
|
-
}
|
|
194
|
-
return '';
|
|
195
|
-
})
|
|
196
|
-
.filter(Boolean)
|
|
197
|
-
.join('');
|
|
186
|
+
function buildUpstreamUrlCandidates(baseUrl, pathSuffix) {
|
|
187
|
+
const safeSuffix = String(pathSuffix || '').replace(/^\/+/, '');
|
|
188
|
+
const candidates = [];
|
|
189
|
+
const push = (url) => {
|
|
190
|
+
if (url && !candidates.includes(url)) {
|
|
191
|
+
candidates.push(url);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
push(joinApiUrl(baseUrl, safeSuffix));
|
|
195
|
+
const trimmed = normalizeBaseUrl(baseUrl);
|
|
196
|
+
if (trimmed && safeSuffix) {
|
|
197
|
+
push(`${trimmed}/${safeSuffix}`);
|
|
198
198
|
}
|
|
199
|
-
return
|
|
199
|
+
return candidates;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// - { type:"input_text"|"input_image", ... }(单个 block)
|
|
207
|
-
// - [{ role, content: [{type:"input_text"|"input_image", ...}] }]
|
|
208
|
-
// - [{ type:"input_text"|"input_image", ... }](视为单条 user 消息)
|
|
209
|
-
if (typeof input === 'string') {
|
|
210
|
-
return [{ role: 'user', content: input }];
|
|
202
|
+
async function proxyRequestJsonWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
|
|
203
|
+
const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
|
|
204
|
+
if (urls.length === 0) {
|
|
205
|
+
return { ok: false, error: 'failed to build upstream URL' };
|
|
211
206
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return content ? [{ role, content }] : [];
|
|
207
|
+
let lastResult = null;
|
|
208
|
+
for (let index = 0; index < urls.length; index += 1) {
|
|
209
|
+
const result = await proxyRequestJson(urls[index], options);
|
|
210
|
+
lastResult = result;
|
|
211
|
+
if (!result.ok) {
|
|
212
|
+
return result;
|
|
219
213
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const content = toChatContent([input]);
|
|
223
|
-
return content ? [{ role: 'user', content }] : [];
|
|
214
|
+
if (!(result.status === 404 || result.status === 405)) {
|
|
215
|
+
return result;
|
|
224
216
|
}
|
|
225
|
-
return [];
|
|
226
217
|
}
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
return lastResult || { ok: false, error: 'failed to build upstream URL' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function stringifyJsonValue(value, fallback = '') {
|
|
222
|
+
if (typeof value === 'string') return value;
|
|
223
|
+
if (value == null) return fallback;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.stringify(value);
|
|
226
|
+
} catch (_) {
|
|
227
|
+
return fallback;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeChatUsageToResponsesUsage(usage) {
|
|
232
|
+
if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return undefined;
|
|
233
|
+
const pickNumber = (...keys) => {
|
|
234
|
+
for (const key of keys) {
|
|
235
|
+
if (Number.isFinite(usage[key])) return usage[key];
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
};
|
|
239
|
+
const inputTokens = pickNumber('input_tokens', 'prompt_tokens');
|
|
240
|
+
const outputTokens = pickNumber('output_tokens', 'completion_tokens');
|
|
241
|
+
const totalTokens = pickNumber('total_tokens');
|
|
242
|
+
const result = {};
|
|
243
|
+
if (inputTokens != null) result.input_tokens = inputTokens;
|
|
244
|
+
if (outputTokens != null) result.output_tokens = outputTokens;
|
|
245
|
+
if (totalTokens != null) result.total_tokens = totalTokens;
|
|
246
|
+
if (usage.input_tokens_details && typeof usage.input_tokens_details === 'object') {
|
|
247
|
+
result.input_tokens_details = usage.input_tokens_details;
|
|
248
|
+
} else if (usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
|
|
249
|
+
result.input_tokens_details = usage.prompt_tokens_details;
|
|
250
|
+
}
|
|
251
|
+
if (usage.output_tokens_details && typeof usage.output_tokens_details === 'object') {
|
|
252
|
+
result.output_tokens_details = usage.output_tokens_details;
|
|
253
|
+
} else if (usage.completion_tokens_details && typeof usage.completion_tokens_details === 'object') {
|
|
254
|
+
result.output_tokens_details = usage.completion_tokens_details;
|
|
255
|
+
}
|
|
256
|
+
return Object.keys(result).length > 0 ? result : usage;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function mapChatFinishReasonToResponses(choice) {
|
|
260
|
+
const finishReason = choice && typeof choice === 'object' && typeof choice.finish_reason === 'string'
|
|
261
|
+
? choice.finish_reason
|
|
262
|
+
: '';
|
|
263
|
+
if (finishReason === 'length') {
|
|
264
|
+
return { status: 'incomplete', incomplete_details: { reason: 'max_output_tokens' } };
|
|
265
|
+
}
|
|
266
|
+
if (finishReason === 'content_filter') {
|
|
267
|
+
return { status: 'incomplete', incomplete_details: { reason: 'content_filter' } };
|
|
229
268
|
}
|
|
269
|
+
return { status: 'completed' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeChatMessageContentToResponsesContent(content, refusal = '') {
|
|
273
|
+
const blocks = [];
|
|
274
|
+
const pushText = (text) => {
|
|
275
|
+
if (typeof text === 'string' && text) {
|
|
276
|
+
blocks.push({ type: 'output_text', text });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
if (typeof content === 'string') {
|
|
280
|
+
pushText(content);
|
|
281
|
+
} else if (Array.isArray(content)) {
|
|
282
|
+
for (const item of content) {
|
|
283
|
+
if (!item) continue;
|
|
284
|
+
if (typeof item === 'string') {
|
|
285
|
+
pushText(item);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (typeof item !== 'object') continue;
|
|
289
|
+
const type = typeof item.type === 'string' ? item.type : '';
|
|
290
|
+
if ((type === 'text' || type === 'output_text') && typeof item.text === 'string') {
|
|
291
|
+
pushText(item.text);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (typeof item.content === 'string') {
|
|
295
|
+
pushText(item.content);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (typeof refusal === 'string' && refusal) {
|
|
300
|
+
blocks.push({ type: 'refusal', refusal });
|
|
301
|
+
}
|
|
302
|
+
return blocks;
|
|
303
|
+
}
|
|
230
304
|
|
|
305
|
+
function buildResponsesPayloadFromChatCompletion(payload, fallbackModel = '') {
|
|
306
|
+
const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
|
307
|
+
const choice = Array.isArray(base.choices) ? base.choices[0] : null;
|
|
308
|
+
const message = choice && typeof choice === 'object' && choice.message && typeof choice.message === 'object'
|
|
309
|
+
? choice.message
|
|
310
|
+
: {};
|
|
311
|
+
const output = [];
|
|
312
|
+
const messageContent = normalizeChatMessageContentToResponsesContent(message.content, message.refusal);
|
|
313
|
+
if (messageContent.length > 0 || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
|
|
314
|
+
output.push({
|
|
315
|
+
type: 'message',
|
|
316
|
+
role: 'assistant',
|
|
317
|
+
content: messageContent.length > 0 ? messageContent : [{ type: 'output_text', text: '' }]
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (Array.isArray(message.tool_calls)) {
|
|
321
|
+
for (const toolCall of message.tool_calls) {
|
|
322
|
+
if (!toolCall || typeof toolCall !== 'object') continue;
|
|
323
|
+
const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : {};
|
|
324
|
+
const name = typeof fn.name === 'string' ? fn.name : '';
|
|
325
|
+
if (!name) continue;
|
|
326
|
+
output.push({
|
|
327
|
+
type: 'function_call',
|
|
328
|
+
call_id: typeof toolCall.id === 'string' && toolCall.id ? toolCall.id : `call_${crypto.randomBytes(8).toString('hex')}`,
|
|
329
|
+
name,
|
|
330
|
+
arguments: stringifyJsonValue(fn.arguments, '{}')
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const finish = mapChatFinishReasonToResponses(choice);
|
|
335
|
+
return ensureResponseMetadata({
|
|
336
|
+
id: typeof base.id === 'string' ? base.id : undefined,
|
|
337
|
+
model: typeof base.model === 'string' ? base.model : fallbackModel,
|
|
338
|
+
status: finish.status,
|
|
339
|
+
...(finish.incomplete_details ? { incomplete_details: finish.incomplete_details } : {}),
|
|
340
|
+
output,
|
|
341
|
+
usage: normalizeChatUsageToResponsesUsage(base.usage)
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizeResponsesInputToChatMessages(input) {
|
|
346
|
+
// 参考 cc-switch 的 Responses 转换形态:message content 保持为消息,function_call /
|
|
347
|
+
// function_call_output 提升为 OpenAI Chat 的 assistant tool_calls / tool 消息。
|
|
231
348
|
const toChatContent = (blocks) => {
|
|
232
349
|
if (!Array.isArray(blocks)) return '';
|
|
233
350
|
const out = [];
|
|
234
351
|
for (const block of blocks) {
|
|
235
352
|
if (!block || typeof block !== 'object') continue;
|
|
236
353
|
const type = typeof block.type === 'string' ? block.type : '';
|
|
237
|
-
if (type === 'input_text' && typeof block.text === 'string') {
|
|
354
|
+
if ((type === 'input_text' || type === 'output_text' || type === 'text') && typeof block.text === 'string') {
|
|
238
355
|
out.push({ type: 'text', text: block.text });
|
|
239
356
|
continue;
|
|
240
357
|
}
|
|
358
|
+
if (type === 'refusal' && typeof block.refusal === 'string') {
|
|
359
|
+
out.push({ type: 'text', text: block.refusal });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
241
362
|
if (type === 'input_image') {
|
|
242
363
|
const raw = block.image_url != null ? block.image_url : block.imageUrl;
|
|
243
364
|
const url = typeof raw === 'string'
|
|
@@ -248,11 +369,6 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
248
369
|
}
|
|
249
370
|
continue;
|
|
250
371
|
}
|
|
251
|
-
// 容错:兼容已是 chat content 的 {type:"text"} / {type:"image_url"}
|
|
252
|
-
if (type === 'text' && typeof block.text === 'string') {
|
|
253
|
-
out.push({ type: 'text', text: block.text });
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
372
|
if (type === 'image_url' && block.image_url) {
|
|
257
373
|
out.push({ type: 'image_url', image_url: block.image_url });
|
|
258
374
|
}
|
|
@@ -261,26 +377,67 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
261
377
|
return out;
|
|
262
378
|
};
|
|
263
379
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
380
|
+
const messageFromResponsesItem = (item) => {
|
|
381
|
+
if (!item || typeof item !== 'object') return null;
|
|
382
|
+
const type = typeof item.type === 'string' ? item.type : '';
|
|
383
|
+
if (type === 'function_call') {
|
|
384
|
+
const name = typeof item.name === 'string' ? item.name : '';
|
|
385
|
+
if (!name) return null;
|
|
386
|
+
return {
|
|
387
|
+
role: 'assistant',
|
|
388
|
+
content: null,
|
|
389
|
+
tool_calls: [{
|
|
390
|
+
id: typeof item.call_id === 'string' && item.call_id ? item.call_id : (typeof item.id === 'string' ? item.id : `call_${crypto.randomBytes(8).toString('hex')}`),
|
|
391
|
+
type: 'function',
|
|
392
|
+
function: {
|
|
393
|
+
name,
|
|
394
|
+
arguments: stringifyJsonValue(item.arguments, '{}')
|
|
395
|
+
}
|
|
396
|
+
}]
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (type === 'function_call_output') {
|
|
400
|
+
const callId = typeof item.call_id === 'string' ? item.call_id : '';
|
|
401
|
+
return {
|
|
402
|
+
role: 'tool',
|
|
403
|
+
tool_call_id: callId,
|
|
404
|
+
content: stringifyJsonValue(item.output, '')
|
|
405
|
+
};
|
|
406
|
+
}
|
|
267
407
|
if (typeof item.role === 'string' && item.content != null) {
|
|
268
408
|
const role = item.role.trim() || 'user';
|
|
269
409
|
const content = Array.isArray(item.content)
|
|
270
410
|
? toChatContent(item.content)
|
|
271
411
|
: item.content;
|
|
272
|
-
|
|
273
|
-
messages.push({ role, content });
|
|
274
|
-
}
|
|
275
|
-
continue;
|
|
412
|
+
return content || content === null ? { role, content } : null;
|
|
276
413
|
}
|
|
414
|
+
if (type) {
|
|
415
|
+
const content = toChatContent([item]);
|
|
416
|
+
return content ? { role: 'user', content } : null;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
if (typeof input === 'string') {
|
|
422
|
+
return [{ role: 'user', content: input }];
|
|
423
|
+
}
|
|
424
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
425
|
+
const message = messageFromResponsesItem(input);
|
|
426
|
+
return message ? [message] : [];
|
|
427
|
+
}
|
|
428
|
+
if (!Array.isArray(input)) {
|
|
429
|
+
return [];
|
|
277
430
|
}
|
|
278
431
|
|
|
432
|
+
const messages = [];
|
|
433
|
+
for (const item of input) {
|
|
434
|
+
const message = messageFromResponsesItem(item);
|
|
435
|
+
if (message) messages.push(message);
|
|
436
|
+
}
|
|
279
437
|
if (messages.length > 0) {
|
|
280
438
|
return messages;
|
|
281
439
|
}
|
|
282
440
|
|
|
283
|
-
// 退化:把 input array 当作单条 user content blocks
|
|
284
441
|
const fallbackContent = toChatContent(input);
|
|
285
442
|
if (fallbackContent) {
|
|
286
443
|
return [{ role: 'user', content: fallbackContent }];
|
|
@@ -288,6 +445,99 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
288
445
|
return [];
|
|
289
446
|
}
|
|
290
447
|
|
|
448
|
+
function normalizeResponsesToolsToChatTools(tools) {
|
|
449
|
+
if (!Array.isArray(tools)) return tools;
|
|
450
|
+
return tools
|
|
451
|
+
.map((tool) => {
|
|
452
|
+
if (!tool || typeof tool !== 'object') return null;
|
|
453
|
+
if (tool.type !== 'function') return tool;
|
|
454
|
+
const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
|
|
455
|
+
? tool.function
|
|
456
|
+
: {};
|
|
457
|
+
const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
|
|
458
|
+
? sourceFn.name.trim()
|
|
459
|
+
: (typeof tool.name === 'string' ? tool.name.trim() : '');
|
|
460
|
+
if (!name) return null;
|
|
461
|
+
const description = typeof sourceFn.description === 'string'
|
|
462
|
+
? sourceFn.description
|
|
463
|
+
: (typeof tool.description === 'string' ? tool.description : undefined);
|
|
464
|
+
const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
|
|
465
|
+
? sourceFn.parameters
|
|
466
|
+
: (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : {});
|
|
467
|
+
const strict = typeof sourceFn.strict === 'boolean'
|
|
468
|
+
? sourceFn.strict
|
|
469
|
+
: (typeof tool.strict === 'boolean' ? tool.strict : undefined);
|
|
470
|
+
const fn = { name, parameters };
|
|
471
|
+
if (description !== undefined) fn.description = description;
|
|
472
|
+
if (strict !== undefined) fn.strict = strict;
|
|
473
|
+
return { type: 'function', function: fn };
|
|
474
|
+
})
|
|
475
|
+
.filter(Boolean);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
|
|
479
|
+
if (!toolChoice || typeof toolChoice !== 'object' || Array.isArray(toolChoice)) return toolChoice;
|
|
480
|
+
if (toolChoice.type === 'function' && typeof toolChoice.name === 'string') {
|
|
481
|
+
return { type: 'function', function: { name: toolChoice.name } };
|
|
482
|
+
}
|
|
483
|
+
return toolChoice;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function buildChatCompletionsBodyFromResponsesPayload(payload) {
|
|
487
|
+
const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
|
488
|
+
const messages = normalizeResponsesInputToChatMessages(source.input);
|
|
489
|
+
const instructions = typeof source.instructions === 'string' ? source.instructions.trim() : '';
|
|
490
|
+
if (instructions) {
|
|
491
|
+
messages.unshift({ role: 'system', content: instructions });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const chatBody = {
|
|
495
|
+
model: typeof source.model === 'string' ? source.model : '',
|
|
496
|
+
messages,
|
|
497
|
+
stream: false
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const passthroughKeys = [
|
|
501
|
+
'frequency_penalty',
|
|
502
|
+
'presence_penalty',
|
|
503
|
+
'response_format',
|
|
504
|
+
'stop',
|
|
505
|
+
'temperature',
|
|
506
|
+
'top_p',
|
|
507
|
+
'tools',
|
|
508
|
+
'tool_choice',
|
|
509
|
+
'logprobs',
|
|
510
|
+
'top_logprobs',
|
|
511
|
+
'kbs',
|
|
512
|
+
'is_online',
|
|
513
|
+
'user',
|
|
514
|
+
'seed',
|
|
515
|
+
'n',
|
|
516
|
+
'modalities',
|
|
517
|
+
'audio',
|
|
518
|
+
'reasoning_effort'
|
|
519
|
+
];
|
|
520
|
+
for (const key of passthroughKeys) {
|
|
521
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
522
|
+
if (key === 'tools') {
|
|
523
|
+
chatBody[key] = normalizeResponsesToolsToChatTools(source[key]);
|
|
524
|
+
} else if (key === 'tool_choice') {
|
|
525
|
+
chatBody[key] = normalizeResponsesToolChoiceToChatToolChoice(source[key]);
|
|
526
|
+
} else {
|
|
527
|
+
chatBody[key] = source[key];
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (Object.prototype.hasOwnProperty.call(source, 'max_tokens')) {
|
|
533
|
+
chatBody.max_tokens = source.max_tokens;
|
|
534
|
+
} else if (source.max_output_tokens != null) {
|
|
535
|
+
chatBody.max_tokens = source.max_output_tokens;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return chatBody;
|
|
539
|
+
}
|
|
540
|
+
|
|
291
541
|
function ensureResponseMetadata(payload) {
|
|
292
542
|
const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
|
293
543
|
const id = typeof base.id === 'string' && base.id.trim()
|
|
@@ -387,6 +637,291 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
387
637
|
writeSse(res, 'done', '[DONE]');
|
|
388
638
|
}
|
|
389
639
|
|
|
640
|
+
function appendChatStreamToolCall(target, toolCall) {
|
|
641
|
+
if (!toolCall || typeof toolCall !== 'object') return;
|
|
642
|
+
const index = Number.isFinite(toolCall.index) ? toolCall.index : target.length;
|
|
643
|
+
if (!target[index]) {
|
|
644
|
+
target[index] = {
|
|
645
|
+
id: '',
|
|
646
|
+
type: 'function',
|
|
647
|
+
function: { name: '', arguments: '' }
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
const current = target[index];
|
|
651
|
+
if (typeof toolCall.id === 'string' && toolCall.id) current.id = toolCall.id;
|
|
652
|
+
if (typeof toolCall.type === 'string' && toolCall.type) current.type = toolCall.type;
|
|
653
|
+
const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : null;
|
|
654
|
+
if (fn) {
|
|
655
|
+
if (typeof fn.name === 'string' && fn.name) current.function.name = fn.name;
|
|
656
|
+
if (typeof fn.arguments === 'string') current.function.arguments += fn.arguments;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function writeChatCompletionChunkAsResponsesSse(state, chunk) {
|
|
661
|
+
if (!chunk || typeof chunk !== 'object') return;
|
|
662
|
+
if (typeof chunk.model === 'string' && chunk.model) {
|
|
663
|
+
state.model = chunk.model;
|
|
664
|
+
}
|
|
665
|
+
const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
|
|
666
|
+
for (const choice of choices) {
|
|
667
|
+
const delta = choice && choice.delta && typeof choice.delta === 'object' ? choice.delta : null;
|
|
668
|
+
if (!delta) continue;
|
|
669
|
+
|
|
670
|
+
if (typeof delta.content === 'string' && delta.content) {
|
|
671
|
+
if (!state.messageItem) {
|
|
672
|
+
state.messageItem = {
|
|
673
|
+
id: `msg_${crypto.randomBytes(8).toString('hex')}`,
|
|
674
|
+
type: 'message',
|
|
675
|
+
role: 'assistant',
|
|
676
|
+
content: [{ type: 'output_text', text: '' }]
|
|
677
|
+
};
|
|
678
|
+
state.output.push(state.messageItem);
|
|
679
|
+
writeSse(state.res, 'response.output_item.added', {
|
|
680
|
+
type: 'response.output_item.added',
|
|
681
|
+
output_index: state.output.length - 1,
|
|
682
|
+
item: state.messageItem
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
state.messageText += delta.content;
|
|
686
|
+
state.messageItem.content[0].text = state.messageText;
|
|
687
|
+
writeSse(state.res, 'response.output_text.delta', {
|
|
688
|
+
type: 'response.output_text.delta',
|
|
689
|
+
item_id: state.messageItem.id,
|
|
690
|
+
output_index: state.output.length - 1,
|
|
691
|
+
content_index: 0,
|
|
692
|
+
delta: delta.content,
|
|
693
|
+
sequence_number: state.nextSeq()
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
698
|
+
for (const toolCall of delta.tool_calls) {
|
|
699
|
+
appendChatStreamToolCall(state.toolCalls, toolCall);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function finishChatStreamResponsesSse(state) {
|
|
706
|
+
if (state.finished) return;
|
|
707
|
+
state.finished = true;
|
|
708
|
+
|
|
709
|
+
if (state.messageItem) {
|
|
710
|
+
const outputIndex = state.output.indexOf(state.messageItem);
|
|
711
|
+
writeSse(state.res, 'response.output_text.done', {
|
|
712
|
+
type: 'response.output_text.done',
|
|
713
|
+
item_id: state.messageItem.id,
|
|
714
|
+
output_index: outputIndex,
|
|
715
|
+
content_index: 0,
|
|
716
|
+
text: state.messageText,
|
|
717
|
+
sequence_number: state.nextSeq()
|
|
718
|
+
});
|
|
719
|
+
writeSse(state.res, 'response.output_item.done', {
|
|
720
|
+
type: 'response.output_item.done',
|
|
721
|
+
output_index: outputIndex,
|
|
722
|
+
item: state.messageItem,
|
|
723
|
+
sequence_number: state.nextSeq()
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for (const toolCall of state.toolCalls) {
|
|
728
|
+
if (!toolCall) continue;
|
|
729
|
+
const item = {
|
|
730
|
+
type: 'function_call',
|
|
731
|
+
call_id: toolCall.id || `call_${crypto.randomBytes(8).toString('hex')}`,
|
|
732
|
+
name: toolCall.function && typeof toolCall.function.name === 'string' ? toolCall.function.name : '',
|
|
733
|
+
arguments: toolCall.function && typeof toolCall.function.arguments === 'string' ? toolCall.function.arguments : ''
|
|
734
|
+
};
|
|
735
|
+
const outputIndex = state.output.length;
|
|
736
|
+
state.output.push(item);
|
|
737
|
+
writeSse(state.res, 'response.output_item.added', {
|
|
738
|
+
type: 'response.output_item.added',
|
|
739
|
+
output_index: outputIndex,
|
|
740
|
+
item
|
|
741
|
+
});
|
|
742
|
+
writeSse(state.res, 'response.output_item.done', {
|
|
743
|
+
type: 'response.output_item.done',
|
|
744
|
+
output_index: outputIndex,
|
|
745
|
+
item,
|
|
746
|
+
sequence_number: state.nextSeq()
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const response = ensureResponseMetadata({
|
|
751
|
+
id: state.responseId,
|
|
752
|
+
model: state.model,
|
|
753
|
+
created_at: state.createdAt,
|
|
754
|
+
status: 'completed',
|
|
755
|
+
output: state.output
|
|
756
|
+
});
|
|
757
|
+
writeSse(state.res, 'response.completed', { type: 'response.completed', response });
|
|
758
|
+
writeSse(state.res, 'done', '[DONE]');
|
|
759
|
+
state.res.end();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
|
|
763
|
+
const parsed = new URL(targetUrl);
|
|
764
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
765
|
+
const bodyText = options.body ? JSON.stringify(options.body) : '';
|
|
766
|
+
const headers = {
|
|
767
|
+
'Accept': 'text/event-stream',
|
|
768
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
769
|
+
...(options.headers || {})
|
|
770
|
+
};
|
|
771
|
+
if (options.body) {
|
|
772
|
+
headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
|
|
773
|
+
}
|
|
774
|
+
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
775
|
+
? Math.max(1000, Number(options.timeoutMs))
|
|
776
|
+
: 30000;
|
|
777
|
+
const res = options.res;
|
|
778
|
+
const model = typeof options.model === 'string' ? options.model : '';
|
|
779
|
+
|
|
780
|
+
return new Promise((resolve) => {
|
|
781
|
+
let settled = false;
|
|
782
|
+
const finish = (value) => {
|
|
783
|
+
if (settled) return;
|
|
784
|
+
settled = true;
|
|
785
|
+
resolve(value);
|
|
786
|
+
};
|
|
787
|
+
const req = transport.request({
|
|
788
|
+
protocol: parsed.protocol,
|
|
789
|
+
hostname: parsed.hostname,
|
|
790
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
791
|
+
method: options.method || 'POST',
|
|
792
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
793
|
+
headers,
|
|
794
|
+
agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
|
|
795
|
+
}, (upstreamRes) => {
|
|
796
|
+
const status = upstreamRes.statusCode || 0;
|
|
797
|
+
const chunks = [];
|
|
798
|
+
const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
|
|
799
|
+
|
|
800
|
+
if (status === 404 || status === 405) {
|
|
801
|
+
upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
|
|
802
|
+
upstreamRes.on('end', () => finish({ retry: true, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (status >= 400) {
|
|
807
|
+
upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
|
|
808
|
+
upstreamRes.on('end', () => finish({ ok: false, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
res.writeHead(200, {
|
|
813
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
814
|
+
'Cache-Control': 'no-cache',
|
|
815
|
+
'Connection': 'keep-alive',
|
|
816
|
+
'X-Accel-Buffering': 'no'
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
if (!/text\/event-stream/i.test(contentType)) {
|
|
820
|
+
upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
|
|
821
|
+
upstreamRes.on('end', () => {
|
|
822
|
+
const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
823
|
+
const parsedJson = parseJsonOrError(text);
|
|
824
|
+
if (parsedJson.error) {
|
|
825
|
+
writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
|
|
826
|
+
writeSse(res, 'done', '[DONE]');
|
|
827
|
+
res.end();
|
|
828
|
+
finish({ ok: true });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
sendResponsesSse(res, buildResponsesPayloadFromChatCompletion(parsedJson.value, model));
|
|
832
|
+
res.end();
|
|
833
|
+
finish({ ok: true });
|
|
834
|
+
});
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
let sequence = 0;
|
|
839
|
+
const state = {
|
|
840
|
+
res,
|
|
841
|
+
responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
|
|
842
|
+
model,
|
|
843
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
844
|
+
output: [],
|
|
845
|
+
messageItem: null,
|
|
846
|
+
messageText: '',
|
|
847
|
+
toolCalls: [],
|
|
848
|
+
finished: false,
|
|
849
|
+
nextSeq: () => {
|
|
850
|
+
sequence += 1;
|
|
851
|
+
return sequence;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
writeSse(res, 'response.created', {
|
|
855
|
+
type: 'response.created',
|
|
856
|
+
response: {
|
|
857
|
+
id: state.responseId,
|
|
858
|
+
model: state.model,
|
|
859
|
+
created_at: state.createdAt
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
let buffer = '';
|
|
864
|
+
const handleEventBlock = (block) => {
|
|
865
|
+
const dataLines = String(block || '')
|
|
866
|
+
.split(/\r?\n/)
|
|
867
|
+
.filter((line) => line.startsWith('data:'))
|
|
868
|
+
.map((line) => line.slice(5).trimStart());
|
|
869
|
+
if (dataLines.length === 0) return;
|
|
870
|
+
const data = dataLines.join('\n').trim();
|
|
871
|
+
if (!data) return;
|
|
872
|
+
if (data === '[DONE]') {
|
|
873
|
+
finishChatStreamResponsesSse(state);
|
|
874
|
+
finish({ ok: true });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const parsedChunk = parseJsonOrError(data);
|
|
878
|
+
if (!parsedChunk.error) {
|
|
879
|
+
writeChatCompletionChunkAsResponsesSse(state, parsedChunk.value);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
upstreamRes.on('data', (chunk) => {
|
|
884
|
+
buffer += chunk.toString('utf-8');
|
|
885
|
+
let boundary = buffer.search(/\r?\n\r?\n/);
|
|
886
|
+
while (boundary >= 0) {
|
|
887
|
+
const block = buffer.slice(0, boundary);
|
|
888
|
+
const match = buffer.slice(boundary).match(/^\r?\n\r?\n/);
|
|
889
|
+
buffer = buffer.slice(boundary + (match ? match[0].length : 2));
|
|
890
|
+
handleEventBlock(block);
|
|
891
|
+
boundary = buffer.search(/\r?\n\r?\n/);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
upstreamRes.on('end', () => {
|
|
895
|
+
if (buffer.trim()) handleEventBlock(buffer);
|
|
896
|
+
finishChatStreamResponsesSse(state);
|
|
897
|
+
finish({ ok: true });
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
req.setTimeout(timeoutMs, () => {
|
|
901
|
+
try { req.destroy(new Error('timeout')); } catch (_) {}
|
|
902
|
+
finish({ ok: false, error: 'timeout' });
|
|
903
|
+
});
|
|
904
|
+
req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
|
|
905
|
+
if (bodyText) req.write(bodyText);
|
|
906
|
+
req.end();
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function streamChatCompletionsAsResponsesSseWithFallbackUrls(baseUrl, pathSuffix, options = {}) {
|
|
911
|
+
const urls = buildUpstreamUrlCandidates(baseUrl, pathSuffix);
|
|
912
|
+
if (urls.length === 0) {
|
|
913
|
+
return { ok: false, error: 'failed to build upstream URL' };
|
|
914
|
+
}
|
|
915
|
+
let lastResult = null;
|
|
916
|
+
for (const url of urls) {
|
|
917
|
+
const result = await streamChatCompletionsAsResponsesSse(url, options);
|
|
918
|
+
lastResult = result;
|
|
919
|
+
if (result && result.retry) continue;
|
|
920
|
+
return result;
|
|
921
|
+
}
|
|
922
|
+
return lastResult || { ok: false, error: 'failed to build upstream URL' };
|
|
923
|
+
}
|
|
924
|
+
|
|
390
925
|
function canListenPort(host, port) {
|
|
391
926
|
return new Promise((resolve) => {
|
|
392
927
|
const tester = net.createServer();
|
|
@@ -761,15 +1296,12 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
761
1296
|
'X-Codexmate-Proxy': '1'
|
|
762
1297
|
};
|
|
763
1298
|
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
body: { ...payload, stream: false }
|
|
771
|
-
})
|
|
772
|
-
: { ok: false, error: 'failed to build upstream URL' };
|
|
1299
|
+
const upstreamResponses = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'responses', {
|
|
1300
|
+
method: 'POST',
|
|
1301
|
+
headers: commonHeaders,
|
|
1302
|
+
timeoutMs,
|
|
1303
|
+
body: { ...payload, stream: false }
|
|
1304
|
+
});
|
|
773
1305
|
|
|
774
1306
|
// 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
|
|
775
1307
|
if (upstreamResponses.ok && upstreamResponses.status >= 200 && upstreamResponses.status < 300) {
|
|
@@ -806,30 +1338,42 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
806
1338
|
}
|
|
807
1339
|
|
|
808
1340
|
if (!upstreamResponses.ok) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1341
|
+
if (!shouldFallbackFromUpstreamResponsesFailure(upstreamResponses.error)) {
|
|
1342
|
+
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1343
|
+
res.end(JSON.stringify({ error: upstreamResponses.error || 'Upstream request failed' }));
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
// Some OpenAI-compatible gateways accept /responses but never complete it.
|
|
1347
|
+
// Treat that as an unsupported Responses endpoint and try the chat fallback.
|
|
812
1348
|
}
|
|
813
1349
|
|
|
814
1350
|
const model = typeof payload.model === 'string' ? payload.model : '';
|
|
815
|
-
const
|
|
816
|
-
const chatBody = {
|
|
817
|
-
model,
|
|
818
|
-
messages,
|
|
819
|
-
stream: false
|
|
820
|
-
};
|
|
821
|
-
if (payload.max_output_tokens != null && chatBody.max_tokens == null) {
|
|
822
|
-
chatBody.max_tokens = payload.max_output_tokens;
|
|
823
|
-
}
|
|
1351
|
+
const chatBody = buildChatCompletionsBodyFromResponsesPayload(payload);
|
|
824
1352
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1353
|
+
if (wantsStream) {
|
|
1354
|
+
const streamingChatBody = { ...chatBody, stream: true };
|
|
1355
|
+
const streamed = await streamChatCompletionsAsResponsesSseWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
|
|
1356
|
+
method: 'POST',
|
|
1357
|
+
headers: commonHeaders,
|
|
1358
|
+
timeoutMs,
|
|
1359
|
+
body: streamingChatBody,
|
|
1360
|
+
res,
|
|
1361
|
+
model
|
|
1362
|
+
});
|
|
1363
|
+
if (!streamed.ok) {
|
|
1364
|
+
if (!res.headersSent) {
|
|
1365
|
+
res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1366
|
+
res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'proxy request failed' }));
|
|
1367
|
+
} else if (!res.writableEnded) {
|
|
1368
|
+
writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'proxy request failed' });
|
|
1369
|
+
writeSse(res, 'done', '[DONE]');
|
|
1370
|
+
res.end();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
829
1373
|
return;
|
|
830
1374
|
}
|
|
831
1375
|
|
|
832
|
-
const upstreamChat = await
|
|
1376
|
+
const upstreamChat = await proxyRequestJsonWithFallbackUrls(upstream.baseUrl, 'chat/completions', {
|
|
833
1377
|
method: 'POST',
|
|
834
1378
|
headers: commonHeaders,
|
|
835
1379
|
timeoutMs,
|
|
@@ -841,6 +1385,12 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
841
1385
|
return;
|
|
842
1386
|
}
|
|
843
1387
|
|
|
1388
|
+
if (upstreamChat.status >= 400) {
|
|
1389
|
+
res.writeHead(upstreamChat.status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1390
|
+
res.end(upstreamChat.bodyText || JSON.stringify({ error: 'Upstream error' }));
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
844
1394
|
const chatJson = parseJsonOrError(upstreamChat.bodyText);
|
|
845
1395
|
if (chatJson.error) {
|
|
846
1396
|
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -848,16 +1398,7 @@ function createBuiltinProxyRuntimeController(deps = {}) {
|
|
|
848
1398
|
return;
|
|
849
1399
|
}
|
|
850
1400
|
|
|
851
|
-
const
|
|
852
|
-
const responsesPayload = ensureResponseMetadata({
|
|
853
|
-
model,
|
|
854
|
-
output: [{
|
|
855
|
-
type: 'message',
|
|
856
|
-
role: 'assistant',
|
|
857
|
-
content: [{ type: 'output_text', text }]
|
|
858
|
-
}],
|
|
859
|
-
usage: chatJson.value && chatJson.value.usage ? chatJson.value.usage : undefined
|
|
860
|
-
});
|
|
1401
|
+
const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model);
|
|
861
1402
|
|
|
862
1403
|
if (wantsStream) {
|
|
863
1404
|
res.writeHead(200, {
|