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.
Files changed (41) hide show
  1. package/README.md +7 -2
  2. package/README.zh.md +7 -2
  3. package/cli/builtin-proxy.js +636 -95
  4. package/cli/openai-bridge.js +497 -5
  5. package/cli.js +75 -29
  6. package/lib/cli-models-utils.js +71 -10
  7. package/package.json +3 -1
  8. package/plugins/prompt-templates/computed.mjs +1 -1
  9. package/plugins/prompt-templates/methods.mjs +0 -66
  10. package/plugins/prompt-templates/overview.mjs +1 -0
  11. package/web-ui/app.js +16 -16
  12. package/web-ui/logic.codex.mjs +56 -0
  13. package/web-ui/logic.sessions.mjs +56 -0
  14. package/web-ui/modules/app.computed.dashboard.mjs +54 -0
  15. package/web-ui/modules/app.computed.session.mjs +48 -0
  16. package/web-ui/modules/app.methods.claude-config.mjs +18 -7
  17. package/web-ui/modules/app.methods.codex-config.mjs +35 -3
  18. package/web-ui/modules/app.methods.providers.mjs +9 -1
  19. package/web-ui/modules/app.methods.session-actions.mjs +2 -5
  20. package/web-ui/modules/app.methods.session-browser.mjs +4 -5
  21. package/web-ui/modules/app.methods.session-trash.mjs +19 -4
  22. package/web-ui/modules/app.methods.startup-claude.mjs +12 -1
  23. package/web-ui/modules/i18n.dict.mjs +28 -32
  24. package/web-ui/modules/provider-url-display.mjs +17 -0
  25. package/web-ui/partials/index/panel-config-claude.html +5 -1
  26. package/web-ui/partials/index/panel-config-codex.html +33 -4
  27. package/web-ui/partials/index/panel-plugins.html +3 -29
  28. package/web-ui/partials/index/panel-sessions.html +0 -10
  29. package/web-ui/partials/index/panel-settings.html +62 -67
  30. package/web-ui/partials/index/panel-usage.html +31 -2
  31. package/web-ui/session-helpers.mjs +2 -2
  32. package/web-ui/styles/base-theme.css +47 -34
  33. package/web-ui/styles/controls-forms.css +27 -28
  34. package/web-ui/styles/layout-shell.css +37 -34
  35. package/web-ui/styles/modals-core.css +12 -10
  36. package/web-ui/styles/navigation-panels.css +36 -35
  37. package/web-ui/styles/responsive.css +4 -4
  38. package/web-ui/styles/sessions-list.css +10 -6
  39. package/web-ui/styles/sessions-usage.css +95 -0
  40. package/web-ui/styles/settings-panel.css +19 -0
  41. package/web-ui/styles/titles-cards.css +90 -26
@@ -369,6 +369,76 @@ function normalizeResponsesInputToChatMessages(input) {
369
369
  return [];
370
370
  }
371
371
 
372
+
373
+ function normalizeResponsesToolsToChatTools(tools) {
374
+ if (!Array.isArray(tools)) return tools;
375
+ return tools
376
+ .map((tool) => {
377
+ if (!tool || typeof tool !== 'object') return null;
378
+ if (tool.type !== 'function') return null;
379
+ const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
380
+ ? tool.function
381
+ : {};
382
+ const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
383
+ ? sourceFn.name.trim()
384
+ : (typeof tool.name === 'string' ? tool.name.trim() : '');
385
+ if (!name) return null;
386
+ const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
387
+ ? sourceFn.parameters
388
+ : (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : {});
389
+ const fn = { name, parameters };
390
+ const description = typeof sourceFn.description === 'string'
391
+ ? sourceFn.description
392
+ : (typeof tool.description === 'string' ? tool.description : undefined);
393
+ const strict = typeof sourceFn.strict === 'boolean'
394
+ ? sourceFn.strict
395
+ : (typeof tool.strict === 'boolean' ? tool.strict : undefined);
396
+ if (description !== undefined) fn.description = description;
397
+ if (strict !== undefined) fn.strict = strict;
398
+ return { type: 'function', function: fn };
399
+ })
400
+ .filter(Boolean);
401
+ }
402
+
403
+ function normalizeResponsesToolChoiceToChatToolChoice(toolChoice) {
404
+ if (!toolChoice || typeof toolChoice !== 'object' || Array.isArray(toolChoice)) return toolChoice;
405
+ if (toolChoice.type === 'function' && typeof toolChoice.name === 'string' && toolChoice.name.trim()) {
406
+ return { type: 'function', function: { name: toolChoice.name.trim() } };
407
+ }
408
+ return toolChoice;
409
+ }
410
+
411
+ function normalizeResponsesToolsForResponsesApi(tools) {
412
+ if (!Array.isArray(tools)) return tools;
413
+ return tools
414
+ .map((tool) => {
415
+ if (!tool || typeof tool !== 'object') return null;
416
+ if (tool.type !== 'function') return null;
417
+ const sourceFn = tool.function && typeof tool.function === 'object' && !Array.isArray(tool.function)
418
+ ? tool.function
419
+ : {};
420
+ const name = typeof sourceFn.name === 'string' && sourceFn.name.trim()
421
+ ? sourceFn.name.trim()
422
+ : (typeof tool.name === 'string' ? tool.name.trim() : '');
423
+ if (!name) return null;
424
+ const out = { type: 'function', name };
425
+ const description = typeof sourceFn.description === 'string'
426
+ ? sourceFn.description
427
+ : (typeof tool.description === 'string' ? tool.description : undefined);
428
+ const parameters = sourceFn.parameters && typeof sourceFn.parameters === 'object' && !Array.isArray(sourceFn.parameters)
429
+ ? sourceFn.parameters
430
+ : (tool.parameters && typeof tool.parameters === 'object' && !Array.isArray(tool.parameters) ? tool.parameters : undefined);
431
+ const strict = typeof sourceFn.strict === 'boolean'
432
+ ? sourceFn.strict
433
+ : (typeof tool.strict === 'boolean' ? tool.strict : undefined);
434
+ if (description !== undefined) out.description = description;
435
+ if (parameters !== undefined) out.parameters = parameters;
436
+ if (strict !== undefined) out.strict = strict;
437
+ return out;
438
+ })
439
+ .filter(Boolean);
440
+ }
441
+
372
442
  function convertResponsesRequestToChatCompletions(payload) {
373
443
  const body = payload && typeof payload === 'object' ? payload : {};
374
444
  const model = typeof body.model === 'string' ? body.model.trim() : '';
@@ -401,12 +471,11 @@ function convertResponsesRequestToChatCompletions(payload) {
401
471
  if (Array.isArray(body.stop) && body.stop.length) {
402
472
  chat.stop = body.stop.filter((item) => typeof item === 'string' && item.trim());
403
473
  }
404
- // Best-effort: pass through tool definitions (most OpenAI-compatible providers accept these fields).
405
474
  if (Array.isArray(body.tools) && body.tools.length) {
406
- chat.tools = body.tools;
475
+ chat.tools = normalizeResponsesToolsToChatTools(body.tools);
407
476
  }
408
477
  if (body.tool_choice !== undefined) {
409
- chat.tool_choice = body.tool_choice;
478
+ chat.tool_choice = normalizeResponsesToolChoiceToChatToolChoice(body.tool_choice);
410
479
  }
411
480
  if (body.response_format !== undefined) {
412
481
  chat.response_format = body.response_format;
@@ -489,6 +558,9 @@ function buildResponsesPayloadFromChatResult(model, text, toolCalls, upstreamPay
489
558
 
490
559
  function ensureResponseMetadata(response) {
491
560
  const payload = response && typeof response === 'object' ? response : {};
561
+ if (typeof payload.object !== 'string' || !payload.object.trim()) {
562
+ payload.object = 'response';
563
+ }
492
564
  if (typeof payload.created_at !== 'number') {
493
565
  payload.created_at = Math.floor(Date.now() / 1000);
494
566
  }
@@ -599,7 +671,11 @@ function extractResponsesOutputText(payload) {
599
671
 
600
672
  function toUpstreamNonStreamingResponsesPayload(payload) {
601
673
  const body = payload && typeof payload === 'object' ? payload : {};
602
- return { ...body, stream: false };
674
+ const normalized = { ...body, stream: false };
675
+ if (Array.isArray(body.tools)) {
676
+ normalized.tools = normalizeResponsesToolsForResponsesApi(body.tools);
677
+ }
678
+ return normalized;
603
679
  }
604
680
 
605
681
  function shouldFallbackFromUpstreamResponses(status, bodyText) {
@@ -616,6 +692,7 @@ function shouldFallbackFromUpstreamResponses(status, bodyText) {
616
692
  if (/unknown (endpoint|route)/i.test(text)) return true;
617
693
  if (/unsupported.*\/?v1\/responses/i.test(text)) return true;
618
694
  if (/does not support.*responses/i.test(text)) return true;
695
+ if (/name['"`]?\s+is a required property/i.test(text) && /tools/i.test(text) && /function/i.test(text)) return true;
619
696
 
620
697
  // Best-effort parse for structured error codes.
621
698
  try {
@@ -627,6 +704,7 @@ function shouldFallbackFromUpstreamResponses(status, bodyText) {
627
704
  if (/unknown (endpoint|route)/i.test(msg)) return true;
628
705
  if (/unsupported.*\/?v1\/responses/i.test(msg)) return true;
629
706
  if (/does not support.*responses/i.test(msg)) return true;
707
+ if (/name['"`]?\s+is a required property/i.test(msg) && /tools/i.test(msg) && /function/i.test(msg)) return true;
630
708
  } catch (_) {}
631
709
 
632
710
  return false;
@@ -639,6 +717,7 @@ function isLoopbackAddress(address) {
639
717
  }
640
718
 
641
719
  function writeSse(res, eventName, dataObj) {
720
+ if (!res || res.writableEnded || res.destroyed) return;
642
721
  if (eventName) {
643
722
  res.write(`event: ${eventName}\n`);
644
723
  }
@@ -649,6 +728,365 @@ function writeSse(res, eventName, dataObj) {
649
728
  res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
650
729
  }
651
730
 
731
+ function appendChatStreamToolCall(target, toolCall) {
732
+ if (!toolCall || typeof toolCall !== 'object') return;
733
+ const index = Number.isFinite(toolCall.index) ? toolCall.index : target.length;
734
+ if (!target[index]) {
735
+ target[index] = {
736
+ id: '',
737
+ type: 'function',
738
+ function: { name: '', arguments: '' }
739
+ };
740
+ }
741
+ const current = target[index];
742
+ if (typeof toolCall.id === 'string' && toolCall.id) current.id = toolCall.id;
743
+ if (typeof toolCall.type === 'string' && toolCall.type) current.type = toolCall.type;
744
+ const fn = toolCall.function && typeof toolCall.function === 'object' ? toolCall.function : null;
745
+ if (fn) {
746
+ if (typeof fn.name === 'string' && fn.name) current.function.name = fn.name;
747
+ if (typeof fn.arguments === 'string') current.function.arguments += fn.arguments;
748
+ }
749
+ }
750
+
751
+ function writeChatCompletionChunkAsResponsesSse(state, chunk) {
752
+ if (!chunk || typeof chunk !== 'object') return;
753
+ if (typeof chunk.model === 'string' && chunk.model) {
754
+ state.model = chunk.model;
755
+ }
756
+ const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
757
+ for (const choice of choices) {
758
+ const delta = choice && choice.delta && typeof choice.delta === 'object' ? choice.delta : null;
759
+ if (!delta) continue;
760
+
761
+ if (typeof delta.content === 'string' && delta.content) {
762
+ if (!state.messageItem) {
763
+ state.messageItem = {
764
+ id: `msg_${crypto.randomBytes(8).toString('hex')}`,
765
+ type: 'message',
766
+ role: 'assistant',
767
+ content: [{ type: 'output_text', text: '' }]
768
+ };
769
+ state.output.push(state.messageItem);
770
+ writeSse(state.res, 'response.output_item.added', {
771
+ type: 'response.output_item.added',
772
+ output_index: state.output.length - 1,
773
+ item: state.messageItem
774
+ });
775
+ }
776
+ state.messageText += delta.content;
777
+ state.messageItem.content[0].text = state.messageText;
778
+ writeSse(state.res, 'response.output_text.delta', {
779
+ type: 'response.output_text.delta',
780
+ item_id: state.messageItem.id,
781
+ output_index: state.output.length - 1,
782
+ content_index: 0,
783
+ delta: delta.content,
784
+ sequence_number: state.nextSeq()
785
+ });
786
+ }
787
+
788
+ if (Array.isArray(delta.tool_calls)) {
789
+ for (const toolCall of delta.tool_calls) {
790
+ appendChatStreamToolCall(state.toolCalls, toolCall);
791
+ }
792
+ }
793
+ }
794
+ }
795
+
796
+ function finishChatStreamResponsesSse(state) {
797
+ if (!state || state.finished) return;
798
+ state.finished = true;
799
+
800
+ if (state.messageItem) {
801
+ const outputIndex = state.output.indexOf(state.messageItem);
802
+ writeSse(state.res, 'response.output_text.done', {
803
+ type: 'response.output_text.done',
804
+ item_id: state.messageItem.id,
805
+ output_index: outputIndex,
806
+ content_index: 0,
807
+ text: state.messageText,
808
+ sequence_number: state.nextSeq()
809
+ });
810
+ writeSse(state.res, 'response.output_item.done', {
811
+ type: 'response.output_item.done',
812
+ output_index: outputIndex,
813
+ item: state.messageItem,
814
+ sequence_number: state.nextSeq()
815
+ });
816
+ }
817
+
818
+ for (const toolCall of state.toolCalls) {
819
+ if (!toolCall) continue;
820
+ const name = toolCall.function && typeof toolCall.function.name === 'string' ? toolCall.function.name : '';
821
+ if (!name) continue;
822
+ const item = {
823
+ type: 'function_call',
824
+ call_id: toolCall.id || `call_${crypto.randomBytes(8).toString('hex')}`,
825
+ name,
826
+ arguments: toolCall.function && typeof toolCall.function.arguments === 'string' ? toolCall.function.arguments : ''
827
+ };
828
+ const outputIndex = state.output.length;
829
+ state.output.push(item);
830
+ writeSse(state.res, 'response.output_item.added', {
831
+ type: 'response.output_item.added',
832
+ output_index: outputIndex,
833
+ item
834
+ });
835
+ writeSse(state.res, 'response.output_item.done', {
836
+ type: 'response.output_item.done',
837
+ output_index: outputIndex,
838
+ item,
839
+ sequence_number: state.nextSeq()
840
+ });
841
+ }
842
+
843
+ const response = ensureResponseMetadata({
844
+ id: state.responseId,
845
+ model: state.model,
846
+ created_at: state.createdAt,
847
+ status: 'completed',
848
+ output: state.output,
849
+ output_text: state.messageText
850
+ });
851
+ writeSse(state.res, 'response.completed', { type: 'response.completed', response });
852
+ writeSse(state.res, 'done', '[DONE]');
853
+ if (!state.res.writableEnded && !state.res.destroyed) {
854
+ state.res.end();
855
+ }
856
+ }
857
+
858
+ function failChatStreamResponsesSse(state, errorMessage) {
859
+ if (!state || state.finished) return;
860
+ state.finished = true;
861
+ writeSse(state.res, 'response.failed', {
862
+ type: 'response.failed',
863
+ response: ensureResponseMetadata({
864
+ id: state.responseId,
865
+ model: state.model,
866
+ created_at: state.createdAt,
867
+ status: 'failed',
868
+ output: state.output,
869
+ output_text: state.messageText
870
+ }),
871
+ error: String(errorMessage || 'upstream stream failed')
872
+ });
873
+ writeSse(state.res, 'done', '[DONE]');
874
+ if (!state.res.writableEnded && !state.res.destroyed) {
875
+ state.res.end();
876
+ }
877
+ }
878
+
879
+ function formatUpstreamStreamError(errorValue) {
880
+ if (!errorValue) return 'upstream stream failed';
881
+ if (typeof errorValue === 'string') return errorValue;
882
+ if (typeof errorValue === 'object') {
883
+ if (typeof errorValue.message === 'string' && errorValue.message) return errorValue.message;
884
+ try { return JSON.stringify(errorValue); } catch (_) {}
885
+ }
886
+ return String(errorValue || 'upstream stream failed');
887
+ }
888
+
889
+ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
890
+ const parsed = new URL(targetUrl);
891
+ const transport = parsed.protocol === 'https:' ? https : http;
892
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
893
+ const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
894
+ ? Math.floor(options.maxBytes)
895
+ : 0;
896
+ const headers = {
897
+ 'Accept': 'text/event-stream',
898
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
899
+ ...(options.headers || {})
900
+ };
901
+ if (options.body) {
902
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
903
+ }
904
+ const timeoutMs = Number.isFinite(options.timeoutMs)
905
+ ? Math.max(1000, Number(options.timeoutMs))
906
+ : 30000;
907
+ const res = options.res;
908
+ const fallbackModel = typeof options.model === 'string' ? options.model : '';
909
+
910
+ return new Promise((resolve) => {
911
+ let settled = false;
912
+ let upstreamReq = null;
913
+ const finish = (value) => {
914
+ if (settled) return;
915
+ settled = true;
916
+ resolve(value);
917
+ };
918
+ const abortUpstream = () => {
919
+ if (upstreamReq) {
920
+ try { upstreamReq.destroy(new Error('client aborted')); } catch (_) {}
921
+ }
922
+ };
923
+ if (res && typeof res.once === 'function') {
924
+ res.once('close', abortUpstream);
925
+ }
926
+
927
+ upstreamReq = transport.request({
928
+ protocol: parsed.protocol,
929
+ hostname: parsed.hostname,
930
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
931
+ method: options.method || 'POST',
932
+ path: `${parsed.pathname}${parsed.search}`,
933
+ headers,
934
+ agent: parsed.protocol === 'https:' ? options.httpsAgent : options.httpAgent
935
+ }, (upstreamRes) => {
936
+ const status = upstreamRes.statusCode || 0;
937
+ const chunks = [];
938
+ let size = 0;
939
+ const contentType = String(upstreamRes.headers && upstreamRes.headers['content-type'] || '');
940
+
941
+ const collectChunk = (chunk) => {
942
+ if (!chunk) return true;
943
+ if (maxBytes > 0) {
944
+ size += chunk.length;
945
+ if (size > maxBytes) {
946
+ chunks.length = 0;
947
+ try { upstreamRes.destroy(new Error('response too large')); } catch (_) {}
948
+ try { upstreamReq.destroy(new Error('response too large')); } catch (_) {}
949
+ finish({ ok: false, status, error: 'response too large' });
950
+ return false;
951
+ }
952
+ }
953
+ chunks.push(chunk);
954
+ return true;
955
+ };
956
+
957
+ if (status >= 400) {
958
+ upstreamRes.on('data', collectChunk);
959
+ upstreamRes.on('end', () => finish({
960
+ ok: false,
961
+ status,
962
+ bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : ''
963
+ }));
964
+ return;
965
+ }
966
+
967
+ if (!res.headersSent) {
968
+ res.writeHead(200, {
969
+ 'Content-Type': 'text/event-stream; charset=utf-8',
970
+ 'Cache-Control': 'no-cache',
971
+ 'Connection': 'keep-alive',
972
+ 'X-Accel-Buffering': 'no'
973
+ });
974
+ if (typeof res.flushHeaders === 'function') res.flushHeaders();
975
+ }
976
+
977
+ if (!/text\/event-stream/i.test(contentType)) {
978
+ upstreamRes.on('data', collectChunk);
979
+ upstreamRes.on('end', () => {
980
+ const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
981
+ const parsedJson = parseJsonOrError(text);
982
+ if (parsedJson.error) {
983
+ writeSse(res, 'response.failed', { type: 'response.failed', error: `invalid upstream response: ${parsedJson.error}` });
984
+ writeSse(res, 'done', '[DONE]');
985
+ if (!res.writableEnded && !res.destroyed) res.end();
986
+ finish({ ok: true });
987
+ return;
988
+ }
989
+ const extracted = extractChatCompletionResult(parsedJson.value);
990
+ sendResponsesSse(res, buildResponsesPayloadFromChatResult(fallbackModel, extracted.text, extracted.toolCalls, parsedJson.value));
991
+ if (!res.writableEnded && !res.destroyed) res.end();
992
+ finish({ ok: true });
993
+ });
994
+ return;
995
+ }
996
+
997
+ let sequence = 0;
998
+ const state = {
999
+ res,
1000
+ responseId: `resp_${crypto.randomBytes(10).toString('hex')}`,
1001
+ model: fallbackModel,
1002
+ createdAt: Math.floor(Date.now() / 1000),
1003
+ output: [],
1004
+ messageItem: null,
1005
+ messageText: '',
1006
+ toolCalls: [],
1007
+ finished: false,
1008
+ sawDone: false,
1009
+ nextSeq: () => {
1010
+ sequence += 1;
1011
+ return sequence;
1012
+ }
1013
+ };
1014
+ writeSse(res, 'response.created', {
1015
+ type: 'response.created',
1016
+ response: {
1017
+ id: state.responseId,
1018
+ model: state.model,
1019
+ created_at: state.createdAt
1020
+ }
1021
+ });
1022
+
1023
+ let buffer = '';
1024
+ const handleEventBlock = (block) => {
1025
+ const dataLines = String(block || '')
1026
+ .split(/\r?\n/)
1027
+ .filter((line) => line.startsWith('data:'))
1028
+ .map((line) => line.slice(5).trimStart());
1029
+ if (dataLines.length === 0) return;
1030
+ const data = dataLines.join('\n').trim();
1031
+ if (!data) return;
1032
+ if (data === '[DONE]') {
1033
+ state.sawDone = true;
1034
+ finishChatStreamResponsesSse(state);
1035
+ finish({ ok: true });
1036
+ return;
1037
+ }
1038
+ const parsedChunk = parseJsonOrError(data);
1039
+ if (!parsedChunk.error) {
1040
+ if (parsedChunk.value && typeof parsedChunk.value === 'object' && parsedChunk.value.error) {
1041
+ failChatStreamResponsesSse(state, formatUpstreamStreamError(parsedChunk.value.error));
1042
+ finish({ ok: true });
1043
+ return;
1044
+ }
1045
+ writeChatCompletionChunkAsResponsesSse(state, parsedChunk.value);
1046
+ }
1047
+ };
1048
+
1049
+ upstreamRes.on('data', (chunk) => {
1050
+ if (!chunk) return;
1051
+ buffer += chunk.toString('utf-8');
1052
+ let boundary = buffer.search(/\r?\n\r?\n/);
1053
+ while (boundary >= 0) {
1054
+ const block = buffer.slice(0, boundary);
1055
+ const match = buffer.slice(boundary).match(/^\r?\n\r?\n/);
1056
+ buffer = buffer.slice(boundary + (match ? match[0].length : 2));
1057
+ handleEventBlock(block);
1058
+ boundary = buffer.search(/\r?\n\r?\n/);
1059
+ }
1060
+ });
1061
+ upstreamRes.on('end', () => {
1062
+ if (buffer.trim()) handleEventBlock(buffer);
1063
+ if (!state.finished && !state.sawDone) {
1064
+ failChatStreamResponsesSse(state, 'upstream stream ended before [DONE]');
1065
+ finish({ ok: true });
1066
+ return;
1067
+ }
1068
+ finishChatStreamResponsesSse(state);
1069
+ finish({ ok: true });
1070
+ });
1071
+ upstreamRes.on('aborted', () => {
1072
+ failChatStreamResponsesSse(state, 'upstream stream aborted');
1073
+ finish({ ok: true });
1074
+ });
1075
+ upstreamRes.on('error', (err) => {
1076
+ failChatStreamResponsesSse(state, err && err.message ? err.message : 'upstream stream failed');
1077
+ finish({ ok: true });
1078
+ });
1079
+ });
1080
+ upstreamReq.setTimeout(timeoutMs, () => {
1081
+ try { upstreamReq.destroy(new Error('timeout')); } catch (_) {}
1082
+ finish({ ok: false, error: 'timeout' });
1083
+ });
1084
+ upstreamReq.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
1085
+ if (bodyText) upstreamReq.write(bodyText);
1086
+ upstreamReq.end();
1087
+ });
1088
+ }
1089
+
652
1090
  async function proxyRequestJson(targetUrl, options = {}) {
653
1091
  const parsed = new URL(targetUrl);
654
1092
  const transport = parsed.protocol === 'https:' ? https : http;
@@ -804,7 +1242,23 @@ function createOpenaiBridgeHttpHandler(options = {}) {
804
1242
  ? upstream.headers
805
1243
  : {};
806
1244
 
807
- if (!normalizedSuffix || normalizedSuffix === 'models') {
1245
+ if (!normalizedSuffix) {
1246
+ if ((req.method || 'GET').toUpperCase() !== 'GET') {
1247
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
1248
+ res.end(JSON.stringify({ error: 'Method Not Allowed' }));
1249
+ return;
1250
+ }
1251
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1252
+ res.end(JSON.stringify({
1253
+ object: 'codexmate.openai_bridge',
1254
+ provider: match.provider,
1255
+ status: 'ok',
1256
+ endpoints: ['/v1/responses', '/v1/models']
1257
+ }));
1258
+ return;
1259
+ }
1260
+
1261
+ if (normalizedSuffix === 'models') {
808
1262
  if ((req.method || 'GET').toUpperCase() !== 'GET') {
809
1263
  res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
810
1264
  res.end(JSON.stringify({ error: 'Method Not Allowed' }));
@@ -862,6 +1316,44 @@ function createOpenaiBridgeHttpHandler(options = {}) {
862
1316
  const acceptHeader = req && req.headers ? (req.headers.accept || req.headers.Accept || '') : '';
863
1317
  const wantsSse = /text\/event-stream/i.test(String(acceptHeader || ''));
864
1318
 
1319
+ if (streamRequested && wantsSse) {
1320
+ const converted = convertResponsesRequestToChatCompletions(responsesRequest);
1321
+ if (converted.error) {
1322
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1323
+ res.end(JSON.stringify({ error: converted.error }));
1324
+ return;
1325
+ }
1326
+ const upstreamUrl = joinApiUrl(upstream.baseUrl, 'chat/completions');
1327
+ const chatBody = { ...converted.chat, stream: true };
1328
+ const streamed = await streamChatCompletionsAsResponsesSse(upstreamUrl, {
1329
+ method: 'POST',
1330
+ body: chatBody,
1331
+ headers: {
1332
+ ...(authHeader ? { Authorization: authHeader } : {}),
1333
+ ...upstreamHeaders
1334
+ },
1335
+ maxBytes: maxUpstreamBytes,
1336
+ httpAgent,
1337
+ httpsAgent,
1338
+ res,
1339
+ model: typeof chatBody.model === 'string' ? chatBody.model : ''
1340
+ });
1341
+ if (!streamed.ok) {
1342
+ if (res.writableEnded || res.destroyed) {
1343
+ return;
1344
+ }
1345
+ if (!res.headersSent) {
1346
+ res.writeHead(streamed.status && streamed.status >= 400 ? streamed.status : 502, { 'Content-Type': 'application/json; charset=utf-8' });
1347
+ res.end(streamed.bodyText || JSON.stringify({ error: streamed.error || 'Upstream request failed' }));
1348
+ } else if (!res.writableEnded && !res.destroyed) {
1349
+ writeSse(res, 'response.failed', { type: 'response.failed', error: streamed.error || streamed.bodyText || 'Upstream request failed' });
1350
+ writeSse(res, 'done', '[DONE]');
1351
+ res.end();
1352
+ }
1353
+ }
1354
+ return;
1355
+ }
1356
+
865
1357
  // Maxx-style behavior: prefer upstream /responses if supported.
866
1358
  // Fallback to /chat/completions conversion when upstream does not implement /responses (404/405).
867
1359
  const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');