codexmate 0.0.43 → 0.0.45

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 (42) hide show
  1. package/README.md +2 -0
  2. package/README.zh.md +2 -0
  3. package/cli/claude-proxy.js +611 -14
  4. package/cli/update.js +77 -7
  5. package/cli.js +188 -21
  6. package/package.json +1 -1
  7. package/web-ui/app.js +36 -3
  8. package/web-ui/index.html +1 -0
  9. package/web-ui/logic.claude.mjs +65 -2
  10. package/web-ui/logic.runtime.mjs +0 -7
  11. package/web-ui/modules/app.computed.index.mjs +3 -1
  12. package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
  13. package/web-ui/modules/app.computed.prompts.mjs +28 -0
  14. package/web-ui/modules/app.computed.session.mjs +23 -1
  15. package/web-ui/modules/app.methods.agents.mjs +50 -4
  16. package/web-ui/modules/app.methods.claude-config.mjs +28 -12
  17. package/web-ui/modules/app.methods.index.mjs +1 -1
  18. package/web-ui/modules/app.methods.install.mjs +129 -1
  19. package/web-ui/modules/app.methods.navigation.mjs +2 -1
  20. package/web-ui/modules/app.methods.session-actions.mjs +17 -2
  21. package/web-ui/modules/app.methods.session-timeline.mjs +0 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +26 -3
  23. package/web-ui/modules/i18n/locales/en.mjs +42 -5
  24. package/web-ui/modules/i18n/locales/ja.mjs +42 -5
  25. package/web-ui/modules/i18n/locales/vi.mjs +51 -0
  26. package/web-ui/modules/i18n/locales/zh.mjs +42 -5
  27. package/web-ui/partials/index/layout-footer.html +1 -1
  28. package/web-ui/partials/index/layout-header.html +64 -0
  29. package/web-ui/partials/index/modal-config-template-agents.html +12 -13
  30. package/web-ui/partials/index/modals-basic.html +18 -1
  31. package/web-ui/partials/index/panel-config-claude.html +4 -7
  32. package/web-ui/partials/index/panel-config-codex.html +2 -6
  33. package/web-ui/partials/index/panel-prompts.html +100 -0
  34. package/web-ui/partials/index/panel-sessions.html +30 -10
  35. package/web-ui/partials/index/panel-usage.html +34 -18
  36. package/web-ui/res/web-ui-render.precompiled.js +579 -149
  37. package/web-ui/styles/controls-forms.css +5 -5
  38. package/web-ui/styles/layout-shell.css +145 -0
  39. package/web-ui/styles/modals-core.css +162 -0
  40. package/web-ui/styles/responsive.css +77 -5
  41. package/web-ui/styles/sessions-toolbar-trash.css +45 -10
  42. package/web-ui/styles/sessions-usage.css +31 -2
@@ -84,6 +84,40 @@ function stringifyAnthropicToolResultContent(content) {
84
84
  return safeJsonStringify(content);
85
85
  }
86
86
 
87
+ function buildOpenAIImageUrlFromAnthropicSource(source) {
88
+ if (!source || typeof source !== 'object') return '';
89
+ if (source.type === 'base64' && typeof source.data === 'string' && source.data.trim()) {
90
+ const mediaType = typeof source.media_type === 'string' && source.media_type.trim()
91
+ ? source.media_type.trim()
92
+ : 'image/png';
93
+ return `data:${mediaType};base64,${source.data.trim()}`;
94
+ }
95
+ if (source.type === 'url' && typeof source.url === 'string' && source.url.trim()) {
96
+ return source.url.trim();
97
+ }
98
+ return '';
99
+ }
100
+
101
+ function collectAnthropicImageBase64(source) {
102
+ if (!source || typeof source !== 'object') return '';
103
+ if (source.type === 'base64' && typeof source.data === 'string' && source.data.trim()) {
104
+ return source.data.trim();
105
+ }
106
+ const url = buildOpenAIImageUrlFromAnthropicSource(source);
107
+ const match = url ? url.match(/^data:[^;,]+;base64,(.+)$/i) : null;
108
+ return match && match[1] ? match[1] : '';
109
+ }
110
+
111
+ function isDroppableAnthropicBridgeBlock(block) {
112
+ const type = block && typeof block.type === 'string' ? block.type : '';
113
+ return type === 'thinking' || type === 'document';
114
+ }
115
+
116
+ function isDroppableAnthropicOllamaBlock(block) {
117
+ const type = block && typeof block.type === 'string' ? block.type : '';
118
+ return type === 'thinking' || type === 'document' || type === 'video';
119
+ }
120
+
87
121
  function appendAnthropicMessageToResponsesInput(target, message) {
88
122
  if (!message || typeof message !== 'object') return;
89
123
  const roleRaw = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
@@ -103,6 +137,13 @@ function appendAnthropicMessageToResponsesInput(target, message) {
103
137
  buffered.push({ type: textType, text: block.text });
104
138
  continue;
105
139
  }
140
+ if (block.type === 'image' && role === 'user') {
141
+ const imageUrl = buildOpenAIImageUrlFromAnthropicSource(block.source);
142
+ if (imageUrl) {
143
+ buffered.push({ type: 'input_image', image_url: imageUrl });
144
+ }
145
+ continue;
146
+ }
106
147
  if (block.type === 'tool_use' && typeof block.name === 'string' && block.name.trim()) {
107
148
  flushBuffered();
108
149
  target.push({
@@ -124,6 +165,9 @@ function appendAnthropicMessageToResponsesInput(target, message) {
124
165
  });
125
166
  continue;
126
167
  }
168
+ if (isDroppableAnthropicBridgeBlock(block)) {
169
+ continue;
170
+ }
127
171
  buffered.push({
128
172
  type: textType,
129
173
  text: `[unsupported anthropic block: ${typeof block.type === 'string' ? block.type : 'unknown'}]`
@@ -133,6 +177,23 @@ function appendAnthropicMessageToResponsesInput(target, message) {
133
177
  flushBuffered();
134
178
  }
135
179
 
180
+ function mapAnthropicToolChoiceToChat(toolChoice) {
181
+ if (!toolChoice) return undefined;
182
+ if (typeof toolChoice === 'string') {
183
+ if (toolChoice === 'auto' || toolChoice === 'none') return toolChoice;
184
+ if (toolChoice === 'any') return 'required';
185
+ return undefined;
186
+ }
187
+ if (!toolChoice || typeof toolChoice !== 'object') return undefined;
188
+ const type = typeof toolChoice.type === 'string' ? toolChoice.type.trim().toLowerCase() : '';
189
+ if (type === 'auto' || type === 'none') return type;
190
+ if (type === 'any') return 'required';
191
+ if (type === 'tool' && typeof toolChoice.name === 'string' && toolChoice.name.trim()) {
192
+ return { type: 'function', function: { name: toolChoice.name.trim() } };
193
+ }
194
+ return undefined;
195
+ }
196
+
136
197
  function mapAnthropicToolChoiceToResponses(toolChoice) {
137
198
  if (!toolChoice) return undefined;
138
199
  if (typeof toolChoice === 'string') {
@@ -218,6 +279,277 @@ function buildBuiltinClaudeResponsesRequest(payload = {}) {
218
279
  return requestBody;
219
280
  }
220
281
 
282
+ function appendAnthropicMessageToChatMessages(target, message) {
283
+ if (!message || typeof message !== 'object') return;
284
+ const roleRaw = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
285
+ const role = roleRaw === 'assistant' ? 'assistant' : 'user';
286
+ let textParts = [];
287
+ let contentParts = [];
288
+ const toolCalls = [];
289
+
290
+ const flushTextPartsToContentParts = () => {
291
+ const content = textParts.join('\n\n').trim();
292
+ textParts = [];
293
+ if (content) {
294
+ contentParts.push({ type: 'text', text: content });
295
+ }
296
+ };
297
+
298
+ const buildContent = () => {
299
+ if (contentParts.length) {
300
+ flushTextPartsToContentParts();
301
+ if (contentParts.length === 1 && contentParts[0].type === 'text') {
302
+ return contentParts[0].text;
303
+ }
304
+ return contentParts;
305
+ }
306
+ return textParts.join('\n\n').trim();
307
+ };
308
+
309
+ const flushText = () => {
310
+ const content = buildContent();
311
+ textParts = [];
312
+ contentParts = [];
313
+ if (!content || (Array.isArray(content) && !content.length)) return;
314
+ target.push({ role, content });
315
+ };
316
+
317
+ for (const block of normalizeAnthropicContentBlocks(message.content)) {
318
+ if (!block || typeof block !== 'object') continue;
319
+ if (block.type === 'text' && typeof block.text === 'string' && block.text) {
320
+ textParts.push(block.text);
321
+ continue;
322
+ }
323
+ if (block.type === 'image' && role === 'user') {
324
+ flushTextPartsToContentParts();
325
+ const imageUrl = buildOpenAIImageUrlFromAnthropicSource(block.source);
326
+ if (imageUrl) {
327
+ contentParts.push({ type: 'image_url', image_url: { url: imageUrl } });
328
+ }
329
+ continue;
330
+ }
331
+ if (isDroppableAnthropicBridgeBlock(block)) {
332
+ continue;
333
+ }
334
+ if (block.type === 'tool_use' && role === 'assistant' && typeof block.name === 'string' && block.name.trim()) {
335
+ toolCalls.push({
336
+ id: typeof block.id === 'string' && block.id.trim()
337
+ ? block.id.trim()
338
+ : `call_${crypto.randomBytes(8).toString('hex')}`,
339
+ type: 'function',
340
+ function: {
341
+ name: block.name.trim(),
342
+ arguments: safeJsonStringify(block.input && typeof block.input === 'object' ? block.input : {})
343
+ }
344
+ });
345
+ continue;
346
+ }
347
+ if (block.type === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id.trim()) {
348
+ flushText();
349
+ target.push({
350
+ role: 'tool',
351
+ tool_call_id: block.tool_use_id.trim(),
352
+ content: stringifyAnthropicToolResultContent(block.content)
353
+ });
354
+ continue;
355
+ }
356
+ textParts.push(`[unsupported anthropic block: ${typeof block.type === 'string' ? block.type : 'unknown'}]`);
357
+ }
358
+
359
+ if (role === 'assistant' && toolCalls.length) {
360
+ const content = buildContent();
361
+ target.push({
362
+ role: 'assistant',
363
+ content: content || null,
364
+ tool_calls: toolCalls
365
+ });
366
+ return;
367
+ }
368
+ flushText();
369
+ }
370
+
371
+ function buildBuiltinClaudeChatCompletionsRequest(payload = {}) {
372
+ const model = typeof payload.model === 'string' ? payload.model.trim() : '';
373
+ if (!model) {
374
+ throw new Error('Anthropic messages 请求缺少 model');
375
+ }
376
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
377
+ if (!messages.length) {
378
+ throw new Error('Anthropic messages 请求缺少 messages');
379
+ }
380
+
381
+ const requestBody = { model, messages: [] };
382
+ const systemText = collectAnthropicTextContent(payload.system);
383
+ if (systemText) {
384
+ requestBody.messages.push({ role: 'system', content: systemText });
385
+ }
386
+ for (const message of messages) {
387
+ appendAnthropicMessageToChatMessages(requestBody.messages, message);
388
+ }
389
+
390
+ const maxTokens = parseInt(String(payload.max_tokens), 10);
391
+ if (Number.isFinite(maxTokens) && maxTokens > 0) {
392
+ requestBody.max_tokens = maxTokens;
393
+ }
394
+ if (Number.isFinite(payload.temperature)) {
395
+ requestBody.temperature = Number(payload.temperature);
396
+ }
397
+ if (Number.isFinite(payload.top_p)) {
398
+ requestBody.top_p = Number(payload.top_p);
399
+ }
400
+ if (Array.isArray(payload.stop_sequences) && payload.stop_sequences.length) {
401
+ const stop = payload.stop_sequences.filter((item) => typeof item === 'string' && item.trim());
402
+ if (stop.length) requestBody.stop = stop;
403
+ }
404
+ if (Array.isArray(payload.tools) && payload.tools.length) {
405
+ requestBody.tools = payload.tools
406
+ .map((tool) => {
407
+ if (!tool || typeof tool !== 'object') return null;
408
+ const name = typeof tool.name === 'string' ? tool.name.trim() : '';
409
+ if (!name) return null;
410
+ return {
411
+ type: 'function',
412
+ function: {
413
+ name,
414
+ description: typeof tool.description === 'string' ? tool.description : '',
415
+ parameters: isPlainObject(tool.input_schema) ? tool.input_schema : { type: 'object', properties: {} }
416
+ }
417
+ };
418
+ })
419
+ .filter(Boolean);
420
+ if (!requestBody.tools.length) delete requestBody.tools;
421
+ }
422
+ const toolChoice = mapAnthropicToolChoiceToChat(payload.tool_choice);
423
+ if (toolChoice !== undefined) {
424
+ requestBody.tool_choice = toolChoice;
425
+ }
426
+ requestBody.stream = false;
427
+ return requestBody;
428
+ }
429
+
430
+
431
+
432
+ function appendAnthropicMessageToOllamaMessages(target, message) {
433
+ if (!message || typeof message !== 'object') return;
434
+ const roleRaw = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
435
+ const role = roleRaw === 'assistant' ? 'assistant' : 'user';
436
+ let textParts = [];
437
+ let images = [];
438
+ const toolCalls = [];
439
+
440
+ const flushText = () => {
441
+ const content = textParts.join('\n\n').trim();
442
+ textParts = [];
443
+ if (!content && !images.length) return;
444
+ const msg = { role, content };
445
+ if (images.length) msg.images = images;
446
+ target.push(msg);
447
+ images = [];
448
+ };
449
+
450
+ for (const block of normalizeAnthropicContentBlocks(message.content)) {
451
+ if (!block || typeof block !== 'object') continue;
452
+ if (block.type === 'text' && typeof block.text === 'string' && block.text) {
453
+ textParts.push(block.text);
454
+ continue;
455
+ }
456
+ if (block.type === 'image' && role === 'user') {
457
+ const image = collectAnthropicImageBase64(block.source);
458
+ if (image) images.push(image);
459
+ continue;
460
+ }
461
+ if (isDroppableAnthropicOllamaBlock(block)) {
462
+ continue;
463
+ }
464
+ if (block.type === 'tool_use' && role === 'assistant' && typeof block.name === 'string' && block.name.trim()) {
465
+ toolCalls.push({
466
+ function: {
467
+ name: block.name.trim(),
468
+ arguments: block.input && typeof block.input === 'object' ? block.input : {}
469
+ }
470
+ });
471
+ continue;
472
+ }
473
+ if (block.type === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id.trim()) {
474
+ flushText();
475
+ target.push({
476
+ role: 'tool',
477
+ content: stringifyAnthropicToolResultContent(block.content),
478
+ tool_call_id: block.tool_use_id.trim()
479
+ });
480
+ continue;
481
+ }
482
+ textParts.push(`[unsupported anthropic block: ${typeof block.type === 'string' ? block.type : 'unknown'}]`);
483
+ }
484
+
485
+ if (role === 'assistant' && toolCalls.length) {
486
+ const content = textParts.join('\n\n').trim();
487
+ const msg = { role: 'assistant', content, tool_calls: toolCalls };
488
+ target.push(msg);
489
+ return;
490
+ }
491
+ flushText();
492
+ }
493
+
494
+ function buildBuiltinClaudeOllamaChatRequest(payload = {}) {
495
+ const model = typeof payload.model === 'string' ? payload.model.trim() : '';
496
+ if (!model) {
497
+ throw new Error('Anthropic messages 请求缺少 model');
498
+ }
499
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
500
+ if (!messages.length) {
501
+ throw new Error('Anthropic messages 请求缺少 messages');
502
+ }
503
+
504
+ const requestBody = { model, messages: [], stream: false };
505
+ const systemText = collectAnthropicTextContent(payload.system);
506
+ if (systemText) {
507
+ requestBody.messages.push({ role: 'system', content: systemText });
508
+ }
509
+ for (const message of messages) {
510
+ appendAnthropicMessageToOllamaMessages(requestBody.messages, message);
511
+ }
512
+
513
+ const options = {};
514
+ const maxTokens = parseInt(String(payload.max_tokens), 10);
515
+ if (Number.isFinite(maxTokens) && maxTokens > 0) options.num_predict = maxTokens;
516
+ if (Number.isFinite(payload.temperature)) options.temperature = Number(payload.temperature);
517
+ if (Number.isFinite(payload.top_p)) options.top_p = Number(payload.top_p);
518
+ if (Array.isArray(payload.stop_sequences) && payload.stop_sequences.length) {
519
+ const stop = payload.stop_sequences.filter((item) => typeof item === 'string' && item.trim());
520
+ if (stop.length) options.stop = stop;
521
+ }
522
+ if (Object.keys(options).length) requestBody.options = options;
523
+
524
+ if (isPlainObject(payload.thinking)) {
525
+ const thinkingType = typeof payload.thinking.type === 'string'
526
+ ? payload.thinking.type.trim().toLowerCase()
527
+ : '';
528
+ if (thinkingType === 'enabled') requestBody.think = true;
529
+ if (thinkingType === 'disabled') requestBody.think = false;
530
+ }
531
+
532
+ if (Array.isArray(payload.tools) && payload.tools.length) {
533
+ requestBody.tools = payload.tools
534
+ .map((tool) => {
535
+ if (!tool || typeof tool !== 'object') return null;
536
+ const name = typeof tool.name === 'string' ? tool.name.trim() : '';
537
+ if (!name) return null;
538
+ return {
539
+ type: 'function',
540
+ function: {
541
+ name,
542
+ description: typeof tool.description === 'string' ? tool.description : '',
543
+ parameters: isPlainObject(tool.input_schema) ? tool.input_schema : { type: 'object', properties: {} }
544
+ }
545
+ };
546
+ })
547
+ .filter(Boolean);
548
+ if (!requestBody.tools.length) delete requestBody.tools;
549
+ }
550
+ return requestBody;
551
+ }
552
+
221
553
  function parseJsonObjectLoose(value) {
222
554
  if (value && typeof value === 'object' && !Array.isArray(value)) {
223
555
  return value;
@@ -296,6 +628,133 @@ function buildAnthropicStopReasonFromResponses(payload, content) {
296
628
  return 'end_turn';
297
629
  }
298
630
 
631
+ function buildAnthropicUsageFromChatCompletion(payload) {
632
+ const usage = payload && payload.usage && typeof payload.usage === 'object' ? payload.usage : {};
633
+ return {
634
+ input_tokens: readResponsesUsageValue(usage.prompt_tokens),
635
+ output_tokens: readResponsesUsageValue(usage.completion_tokens)
636
+ };
637
+ }
638
+
639
+ function normalizeChatMessageContentText(content) {
640
+ if (typeof content === 'string') return content;
641
+ if (Array.isArray(content)) {
642
+ return content.map((item) => {
643
+ if (typeof item === 'string') return item;
644
+ if (item && typeof item === 'object' && typeof item.text === 'string') return item.text;
645
+ return '';
646
+ }).filter(Boolean).join('\n\n');
647
+ }
648
+ return '';
649
+ }
650
+
651
+ function buildAnthropicStopReasonFromChatChoice(choice, content) {
652
+ if (Array.isArray(content) && content.some((item) => item && item.type === 'tool_use')) {
653
+ return 'tool_use';
654
+ }
655
+ const finishReason = choice && typeof choice.finish_reason === 'string' ? choice.finish_reason : '';
656
+ if (finishReason === 'length') return 'max_tokens';
657
+ if (finishReason === 'tool_calls' || finishReason === 'function_call') return 'tool_use';
658
+ return 'end_turn';
659
+ }
660
+
661
+ function buildAnthropicMessageFromChatCompletion(payload, requestPayload = {}) {
662
+ const choices = Array.isArray(payload && payload.choices) ? payload.choices : [];
663
+ const choice = choices.find((item) => item && item.message) || choices[0] || {};
664
+ const chatMessage = choice && choice.message && typeof choice.message === 'object' ? choice.message : {};
665
+ const content = [];
666
+ const text = normalizeChatMessageContentText(chatMessage.content);
667
+ if (text) {
668
+ content.push({ type: 'text', text });
669
+ }
670
+ const toolCalls = Array.isArray(chatMessage.tool_calls) ? chatMessage.tool_calls : [];
671
+ for (const call of toolCalls) {
672
+ if (!call || typeof call !== 'object') continue;
673
+ const fn = call.function && typeof call.function === 'object' ? call.function : {};
674
+ const name = typeof fn.name === 'string' ? fn.name : '';
675
+ if (!name) continue;
676
+ content.push({
677
+ type: 'tool_use',
678
+ id: typeof call.id === 'string' && call.id.trim()
679
+ ? call.id.trim()
680
+ : `toolu_${crypto.randomBytes(8).toString('hex')}`,
681
+ name,
682
+ input: parseJsonObjectLoose(fn.arguments)
683
+ });
684
+ }
685
+ if (!content.length) {
686
+ const fallbackText = extractModelResponseText(payload);
687
+ if (fallbackText) content.push({ type: 'text', text: fallbackText });
688
+ }
689
+ const usage = buildAnthropicUsageFromChatCompletion(payload);
690
+ return {
691
+ id: typeof payload.id === 'string' && payload.id.trim()
692
+ ? payload.id.trim()
693
+ : `msg_${crypto.randomBytes(8).toString('hex')}`,
694
+ type: 'message',
695
+ role: 'assistant',
696
+ model: typeof payload.model === 'string' && payload.model.trim()
697
+ ? payload.model.trim()
698
+ : (typeof requestPayload.model === 'string' ? requestPayload.model : ''),
699
+ content,
700
+ stop_reason: buildAnthropicStopReasonFromChatChoice(choice, content),
701
+ stop_sequence: null,
702
+ usage
703
+ };
704
+ }
705
+
706
+
707
+ function buildAnthropicMessageFromOllamaChat(payload, requestPayload = {}) {
708
+ const ollamaMessage = payload && payload.message && typeof payload.message === 'object' ? payload.message : {};
709
+ const content = [];
710
+ if (typeof ollamaMessage.thinking === 'string' && ollamaMessage.thinking) {
711
+ content.push({ type: 'thinking', thinking: ollamaMessage.thinking });
712
+ }
713
+ if (typeof ollamaMessage.content === 'string' && ollamaMessage.content) {
714
+ content.push({ type: 'text', text: ollamaMessage.content });
715
+ }
716
+ const toolCalls = Array.isArray(ollamaMessage.tool_calls) ? ollamaMessage.tool_calls : [];
717
+ for (const call of toolCalls) {
718
+ if (!call || typeof call !== 'object') continue;
719
+ const fn = call.function && typeof call.function === 'object' ? call.function : {};
720
+ const name = typeof fn.name === 'string' ? fn.name : '';
721
+ if (!name) continue;
722
+ content.push({
723
+ type: 'tool_use',
724
+ id: typeof call.id === 'string' && call.id.trim()
725
+ ? call.id.trim()
726
+ : `toolu_${crypto.randomBytes(8).toString('hex')}`,
727
+ name,
728
+ input: parseJsonObjectLoose(fn.arguments)
729
+ });
730
+ }
731
+ if (!content.length) {
732
+ const fallbackText = extractModelResponseText(payload);
733
+ if (fallbackText) content.push({ type: 'text', text: fallbackText });
734
+ }
735
+ const usage = {
736
+ input_tokens: readResponsesUsageValue(payload && payload.prompt_eval_count),
737
+ output_tokens: readResponsesUsageValue(payload && payload.eval_count)
738
+ };
739
+ const doneReason = payload && typeof payload.done_reason === 'string' ? payload.done_reason : '';
740
+ return {
741
+ id: typeof payload.id === 'string' && payload.id.trim()
742
+ ? payload.id.trim()
743
+ : `msg_${crypto.randomBytes(8).toString('hex')}`,
744
+ type: 'message',
745
+ role: 'assistant',
746
+ model: typeof payload.model === 'string' && payload.model.trim()
747
+ ? payload.model.trim()
748
+ : (typeof requestPayload.model === 'string' ? requestPayload.model : ''),
749
+ content,
750
+ stop_reason: Array.isArray(content) && content.some((item) => item && item.type === 'tool_use')
751
+ ? 'tool_use'
752
+ : (doneReason === 'length' || doneReason === 'max_tokens' ? 'max_tokens' : 'end_turn'),
753
+ stop_sequence: null,
754
+ usage
755
+ };
756
+ }
757
+
299
758
  function buildAnthropicMessageFromResponses(payload, requestPayload = {}) {
300
759
  const content = collectAnthropicContentFromResponsesOutput(payload);
301
760
  const usage = buildAnthropicUsageFromResponses(payload);
@@ -360,6 +819,28 @@ function buildAnthropicStreamEvents(message) {
360
819
  events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
361
820
  return;
362
821
  }
822
+ if (block.type === 'thinking') {
823
+ events.push({
824
+ event: 'content_block_start',
825
+ data: {
826
+ type: 'content_block_start',
827
+ index,
828
+ content_block: { type: 'thinking', thinking: '' }
829
+ }
830
+ });
831
+ if (typeof block.thinking === 'string' && block.thinking) {
832
+ events.push({
833
+ event: 'content_block_delta',
834
+ data: {
835
+ type: 'content_block_delta',
836
+ index,
837
+ delta: { type: 'thinking_delta', thinking: block.thinking }
838
+ }
839
+ });
840
+ }
841
+ events.push({ event: 'content_block_stop', data: { type: 'content_block_stop', index } });
842
+ return;
843
+ }
363
844
  if (block.type === 'tool_use') {
364
845
  events.push({
365
846
  event: 'content_block_start',
@@ -423,6 +904,15 @@ function buildAnthropicModelsPayload(upstreamPayload) {
423
904
  };
424
905
  }
425
906
 
907
+ function joinBuiltinClaudeProxyUpstreamUrl(baseUrl, pathSuffix) {
908
+ const suffix = typeof pathSuffix === 'string' ? pathSuffix.replace(/^\/+/, '') : '';
909
+ if (suffix === 'api/tags' || suffix === 'api/chat') {
910
+ const normalized = normalizeBaseUrl(baseUrl);
911
+ return normalized ? `${normalized}/${suffix}` : '';
912
+ }
913
+ return joinApiUrl(baseUrl, suffix);
914
+ }
915
+
426
916
  function createBuiltinClaudeProxyRuntimeController(deps = {}) {
427
917
  const {
428
918
  BUILTIN_CLAUDE_PROXY_SETTINGS_FILE,
@@ -433,7 +923,9 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
433
923
  HTTPS_KEEP_ALIVE_AGENT,
434
924
  readConfigOrVirtualDefault,
435
925
  resolveBuiltinProxyProviderName,
436
- resolveAuthTokenFromCurrentProfile
926
+ resolveAuthTokenFromCurrentProfile,
927
+ OPENAI_BRIDGE_SETTINGS_FILE,
928
+ resolveOpenaiBridgeUpstream
437
929
  } = deps;
438
930
 
439
931
  if (!BUILTIN_CLAUDE_PROXY_SETTINGS_FILE) {
@@ -462,18 +954,32 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
462
954
  const host = typeof merged.host === 'string' ? merged.host.trim() : '';
463
955
  const port = parseInt(String(merged.port), 10);
464
956
  const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
957
+ const upstreamProviderName = typeof merged.upstreamProviderName === 'string' ? merged.upstreamProviderName.trim() : '';
958
+ const upstreamBaseUrl = typeof merged.upstreamBaseUrl === 'string' ? merged.upstreamBaseUrl.trim() : '';
959
+ const upstreamApiKey = typeof merged.upstreamApiKey === 'string' ? merged.upstreamApiKey.trim() : '';
465
960
  const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
961
+ const targetApiRaw = typeof merged.targetApi === 'string' ? merged.targetApi.trim().toLowerCase() : '';
466
962
  const timeoutMs = parseInt(String(merged.timeoutMs), 10);
467
963
  const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' || authSourceRaw === 'request'
468
964
  ? authSourceRaw
469
965
  : 'provider';
966
+ let targetApi = 'responses';
967
+ if (targetApiRaw === 'chat_completions' || targetApiRaw === 'chat-completions' || targetApiRaw === 'chat/completions') {
968
+ targetApi = 'chat_completions';
969
+ } else if (targetApiRaw === 'ollama') {
970
+ targetApi = 'ollama';
971
+ }
470
972
 
471
973
  return {
472
974
  enabled: merged.enabled !== false,
473
975
  host: host || DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
474
976
  port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.port,
475
977
  provider,
978
+ upstreamProviderName,
979
+ upstreamBaseUrl,
980
+ upstreamApiKey,
476
981
  authSource,
982
+ targetApi,
477
983
  timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
478
984
  ? timeoutMs
479
985
  : DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs
@@ -507,6 +1013,17 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
507
1013
  ...merged,
508
1014
  provider: finalProvider
509
1015
  };
1016
+ const payloadObject = isPlainObject(payload) ? payload : {};
1017
+ const payloadHasDirectUpstream = Object.prototype.hasOwnProperty.call(payloadObject, 'upstreamBaseUrl');
1018
+ const payloadSelectsProvider = Object.prototype.hasOwnProperty.call(payloadObject, 'provider')
1019
+ || Object.prototype.hasOwnProperty.call(payloadObject, 'upstreamProviderName');
1020
+ const payloadSelectsResponses = Object.prototype.hasOwnProperty.call(payloadObject, 'targetApi')
1021
+ && normalized.targetApi === 'responses';
1022
+ if (!payloadHasDirectUpstream && (payloadSelectsProvider || payloadSelectsResponses)) {
1023
+ normalized.upstreamProviderName = '';
1024
+ normalized.upstreamBaseUrl = '';
1025
+ normalized.upstreamApiKey = '';
1026
+ }
510
1027
 
511
1028
  if (!options.skipWrite) {
512
1029
  writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, normalized);
@@ -539,9 +1056,36 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
539
1056
  return { error: `上游 provider 不存在: ${providerName}` };
540
1057
  }
541
1058
 
542
- const wireApi = normalizeWireApi(provider.wire_api);
543
- if (wireApi !== 'responses') {
544
- return { error: `Claude 兼容代理仅支持上游 responses provider: ${providerName}` };
1059
+ const targetApi = settings.targetApi === 'chat_completions'
1060
+ ? 'chat_completions'
1061
+ : (settings.targetApi === 'ollama' ? 'ollama' : 'responses');
1062
+ if (targetApi === 'responses') {
1063
+ const wireApi = normalizeWireApi(provider.wire_api);
1064
+ if (wireApi !== 'responses') {
1065
+ return { error: `Claude 兼容代理仅支持上游 responses provider: ${providerName}` };
1066
+ }
1067
+ }
1068
+
1069
+ if (targetApi === 'chat_completions'
1070
+ && provider.codexmate_bridge === 'openai'
1071
+ && typeof resolveOpenaiBridgeUpstream === 'function'
1072
+ && OPENAI_BRIDGE_SETTINGS_FILE) {
1073
+ const bridgeUpstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, providerName);
1074
+ if (!bridgeUpstream || bridgeUpstream.error) {
1075
+ return { error: bridgeUpstream && bridgeUpstream.error ? bridgeUpstream.error : `OpenAI bridge 配置未找到: ${providerName}` };
1076
+ }
1077
+ const bridgeBaseUrl = typeof bridgeUpstream.baseUrl === 'string' ? bridgeUpstream.baseUrl.trim() : '';
1078
+ if (!bridgeBaseUrl || !isValidHttpUrl(bridgeBaseUrl)) {
1079
+ return { error: `OpenAI 转换上游 base_url 无效: ${providerName}` };
1080
+ }
1081
+ const bridgeToken = typeof bridgeUpstream.apiKey === 'string' ? bridgeUpstream.apiKey.trim() : '';
1082
+ return {
1083
+ providerName,
1084
+ baseUrl: normalizeBaseUrl(bridgeBaseUrl),
1085
+ authHeader: bridgeToken ? (/^bearer\s+/i.test(bridgeToken) ? bridgeToken : `Bearer ${bridgeToken}`) : '',
1086
+ extraHeaders: isPlainObject(bridgeUpstream.headers) ? bridgeUpstream.headers : {},
1087
+ targetApi
1088
+ };
545
1089
  }
546
1090
 
547
1091
  const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
@@ -567,7 +1111,43 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
567
1111
  return {
568
1112
  providerName,
569
1113
  baseUrl: normalizeBaseUrl(baseUrl),
570
- authHeader
1114
+ authHeader,
1115
+ extraHeaders: {},
1116
+ targetApi
1117
+ };
1118
+ }
1119
+
1120
+ function resolveBuiltinClaudeProxyDirectUpstream(settings, payload = {}) {
1121
+ const targetApi = settings.targetApi === 'chat_completions'
1122
+ ? 'chat_completions'
1123
+ : (settings.targetApi === 'ollama' ? 'ollama' : 'responses');
1124
+ const baseUrl = typeof payload.upstreamBaseUrl === 'string' && payload.upstreamBaseUrl.trim()
1125
+ ? payload.upstreamBaseUrl.trim()
1126
+ : (typeof settings.upstreamBaseUrl === 'string' ? settings.upstreamBaseUrl.trim() : '');
1127
+ if (!baseUrl) {
1128
+ return null;
1129
+ }
1130
+ if (!isValidHttpUrl(baseUrl)) {
1131
+ return { error: 'Claude 兼容代理上游 base_url 无效' };
1132
+ }
1133
+ const token = typeof payload.upstreamApiKey === 'string' && payload.upstreamApiKey.trim()
1134
+ ? payload.upstreamApiKey.trim()
1135
+ : (typeof settings.upstreamApiKey === 'string' ? settings.upstreamApiKey.trim() : '');
1136
+ let authHeader = '';
1137
+ if (token) {
1138
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
1139
+ }
1140
+ const providerName = typeof payload.upstreamProviderName === 'string' && payload.upstreamProviderName.trim()
1141
+ ? payload.upstreamProviderName.trim()
1142
+ : (typeof settings.upstreamProviderName === 'string' && settings.upstreamProviderName.trim()
1143
+ ? settings.upstreamProviderName.trim()
1144
+ : 'claude-config');
1145
+ return {
1146
+ providerName,
1147
+ baseUrl: normalizeBaseUrl(baseUrl),
1148
+ authHeader,
1149
+ extraHeaders: {},
1150
+ targetApi
571
1151
  };
572
1152
  }
573
1153
 
@@ -656,7 +1236,7 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
656
1236
 
657
1237
  function requestBuiltinClaudeProxyUpstream(upstream, requestOptions = {}) {
658
1238
  const pathSuffix = typeof requestOptions.pathSuffix === 'string' ? requestOptions.pathSuffix : '';
659
- const targetBase = joinApiUrl(upstream.baseUrl, pathSuffix);
1239
+ const targetBase = joinBuiltinClaudeProxyUpstreamUrl(upstream.baseUrl, pathSuffix);
660
1240
  if (!targetBase) {
661
1241
  return Promise.reject(new Error('failed to build upstream URL'));
662
1242
  }
@@ -770,7 +1350,7 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
770
1350
  ok: true,
771
1351
  upstreamProvider: upstream.providerName,
772
1352
  upstreamBaseUrl: upstream.baseUrl,
773
- mode: 'anthropic-to-responses'
1353
+ mode: upstream.targetApi === 'chat_completions' ? 'anthropic-to-chat-completions' : (upstream.targetApi === 'ollama' ? 'anthropic-to-ollama' : 'anthropic-to-responses')
774
1354
  });
775
1355
  res.writeHead(200, {
776
1356
  'Content-Type': 'application/json; charset=utf-8',
@@ -794,8 +1374,9 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
794
1374
  }
795
1375
  const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
796
1376
  method: 'GET',
797
- pathSuffix: 'models',
1377
+ pathSuffix: upstream.targetApi === 'ollama' ? 'api/tags' : 'models',
798
1378
  authHeader: authResult.authHeader,
1379
+ headers: upstream.extraHeaders,
799
1380
  timeoutMs: settings.timeoutMs
800
1381
  });
801
1382
  if (upstreamResponse.statusCode < 200 || upstreamResponse.statusCode >= 300) {
@@ -828,12 +1409,20 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
828
1409
  }
829
1410
 
830
1411
  const payload = await readJsonRequestBody(req);
831
- const upstreamRequestBody = buildBuiltinClaudeResponsesRequest(payload);
1412
+ const activeTargetApi = upstream.targetApi === 'ollama' || settings.targetApi === 'ollama'
1413
+ ? 'ollama'
1414
+ : (upstream.targetApi === 'chat_completions' || settings.targetApi === 'chat_completions' ? 'chat_completions' : 'responses');
1415
+ const upstreamRequestBody = activeTargetApi === 'ollama'
1416
+ ? buildBuiltinClaudeOllamaChatRequest(payload)
1417
+ : (activeTargetApi === 'chat_completions'
1418
+ ? buildBuiltinClaudeChatCompletionsRequest(payload)
1419
+ : buildBuiltinClaudeResponsesRequest(payload));
832
1420
  const upstreamResponse = await requestBuiltinClaudeProxyUpstream(upstream, {
833
1421
  method: 'POST',
834
- pathSuffix: 'responses',
1422
+ pathSuffix: activeTargetApi === 'ollama' ? 'api/chat' : (activeTargetApi === 'chat_completions' ? 'chat/completions' : 'responses'),
835
1423
  body: upstreamRequestBody,
836
1424
  authHeader: authResult.authHeader,
1425
+ headers: upstream.extraHeaders,
837
1426
  timeoutMs: settings.timeoutMs
838
1427
  });
839
1428
 
@@ -847,7 +1436,11 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
847
1436
  return;
848
1437
  }
849
1438
 
850
- const anthropicMessage = buildAnthropicMessageFromResponses(upstreamResponse.payload || {}, payload);
1439
+ const anthropicMessage = activeTargetApi === 'ollama'
1440
+ ? buildAnthropicMessageFromOllamaChat(upstreamResponse.payload || {}, payload)
1441
+ : (activeTargetApi === 'chat_completions'
1442
+ ? buildAnthropicMessageFromChatCompletion(upstreamResponse.payload || {}, payload)
1443
+ : buildAnthropicMessageFromResponses(upstreamResponse.payload || {}, payload));
851
1444
  if (payload.stream === true) {
852
1445
  writeAnthropicStreamEvents(res, anthropicMessage);
853
1446
  return;
@@ -934,7 +1527,7 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
934
1527
  return { error: saveResult.error };
935
1528
  }
936
1529
  const settings = saveResult.settings;
937
- const upstream = resolveBuiltinClaudeProxyUpstream(settings);
1530
+ const upstream = resolveBuiltinClaudeProxyDirectUpstream(settings, payload) || resolveBuiltinClaudeProxyUpstream(settings);
938
1531
  if (upstream.error) {
939
1532
  return { error: upstream.error };
940
1533
  }
@@ -946,7 +1539,7 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
946
1539
  running: true,
947
1540
  listenUrl: runtime.listenUrl,
948
1541
  upstreamProvider: upstream.providerName,
949
- mode: 'anthropic-to-responses',
1542
+ mode: upstream.targetApi === 'chat_completions' ? 'anthropic-to-chat-completions' : (upstream.targetApi === 'ollama' ? 'anthropic-to-ollama' : 'anthropic-to-responses'),
950
1543
  settings
951
1544
  };
952
1545
  } catch (e) {
@@ -995,7 +1588,7 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
995
1588
  listenUrl: runtime.listenUrl,
996
1589
  upstreamProvider: runtime.upstream.providerName,
997
1590
  upstreamBaseUrl: runtime.upstream.baseUrl,
998
- mode: 'anthropic-to-responses'
1591
+ mode: runtime.upstream.targetApi === 'chat_completions' ? 'anthropic-to-chat-completions' : (runtime.upstream.targetApi === 'ollama' ? 'anthropic-to-ollama' : 'anthropic-to-responses')
999
1592
  }
1000
1593
  : null
1001
1594
  };
@@ -1016,7 +1609,11 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
1016
1609
  module.exports = {
1017
1610
  createBuiltinClaudeProxyRuntimeController,
1018
1611
  buildBuiltinClaudeResponsesRequest,
1612
+ buildBuiltinClaudeChatCompletionsRequest,
1613
+ buildBuiltinClaudeOllamaChatRequest,
1019
1614
  buildAnthropicMessageFromResponses,
1615
+ buildAnthropicMessageFromChatCompletion,
1616
+ buildAnthropicMessageFromOllamaChat,
1020
1617
  buildAnthropicStreamEvents,
1021
1618
  buildAnthropicModelsPayload
1022
1619
  };