@minniexcode/codex-switch 0.1.5 → 0.2.0
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/dist/app/add-provider.js +10 -16
- package/dist/app/bridge.js +8 -13
- package/dist/app/get-status.js +15 -12
- package/dist/app/run-doctor.js +17 -18
- package/dist/app/switch-provider.js +6 -11
- package/dist/commands/handlers.js +32 -69
- package/dist/domain/providers.js +9 -9
- package/dist/runtime/copilot-adapter.js +6 -1
- package/dist/runtime/copilot-bridge.js +109 -76
- package/dist/runtime/copilot-http-bridge-worker.js +228 -0
- package/dist/runtime/copilot-token.js +294 -0
- package/docs/Design/codex-switch-v0.2.0-design.md +56 -0
- package/package.json +1 -1
|
@@ -163,13 +163,13 @@ async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
|
|
|
163
163
|
/**
|
|
164
164
|
* Starts or reuses a Copilot bridge worker, then verifies its health before returning.
|
|
165
165
|
*/
|
|
166
|
-
async function ensureCopilotBridge(providerName, provider, runtimeDir, runtimesDir) {
|
|
167
|
-
return startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir);
|
|
166
|
+
async function ensureCopilotBridge(providerName, provider, runtimeDir, runtimesDir, toolHomeDir) {
|
|
167
|
+
return startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir, toolHomeDir);
|
|
168
168
|
}
|
|
169
169
|
/**
|
|
170
170
|
* Starts or reuses a Copilot bridge worker and reports the chosen port.
|
|
171
171
|
*/
|
|
172
|
-
async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir) {
|
|
172
|
+
async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir, toolHomeDir) {
|
|
173
173
|
if (!(0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
174
174
|
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider is not backed by a Copilot bridge runtime.", {
|
|
175
175
|
provider: providerName,
|
|
@@ -221,7 +221,7 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
221
221
|
}
|
|
222
222
|
const selectedPort = await selectBridgePort(runtime.bridgeHost, runtime.bridgePort);
|
|
223
223
|
const selectedBaseUrl = `http://${runtime.bridgeHost}:${selectedPort}${runtime.bridgePath}`;
|
|
224
|
-
const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
|
|
224
|
+
const workerPath = path.join(__dirname, "copilot-http-bridge-worker.js");
|
|
225
225
|
ensureBridgeLogFile(logPath);
|
|
226
226
|
appendBridgeLifecycleLog(logPath, `worker start provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)} replaced=${String(replaced)}`);
|
|
227
227
|
let child;
|
|
@@ -239,6 +239,7 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
239
239
|
CODEX_SWITCH_RUNTIME_DIR: runtimeDir ?? "",
|
|
240
240
|
CODEX_SWITCH_RUNTIMES_DIR: runtimesDir ?? "",
|
|
241
241
|
CODEX_SWITCH_BRIDGE_LOG_PATH: logPath,
|
|
242
|
+
CODEX_SWITCH_TOOL_HOME_DIR: toolHomeDir ?? "",
|
|
242
243
|
},
|
|
243
244
|
});
|
|
244
245
|
}
|
|
@@ -349,23 +350,27 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
349
350
|
connection: "keep-alive",
|
|
350
351
|
});
|
|
351
352
|
const heartbeat = startSseHeartbeat(response);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
353
|
+
try {
|
|
354
|
+
const payload = await context.executeChatCompletion(body, {
|
|
355
|
+
timeoutMs,
|
|
356
|
+
onTextDelta: (delta) => {
|
|
357
|
+
response.write(`data: ${JSON.stringify({
|
|
358
|
+
choices: [
|
|
359
|
+
{
|
|
360
|
+
index: 0,
|
|
361
|
+
delta: { content: delta },
|
|
362
|
+
finish_reason: null,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
})}\n\n`);
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
response.write("data: [DONE]\n\n");
|
|
369
|
+
response.end();
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
clearInterval(heartbeat);
|
|
373
|
+
}
|
|
369
374
|
return;
|
|
370
375
|
}
|
|
371
376
|
const payload = await context.executeChatCompletion(body, { timeoutMs });
|
|
@@ -389,31 +394,36 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
389
394
|
const responseId = `resp_${Date.now()}`;
|
|
390
395
|
const messageId = buildResponsesMessageId(responseId);
|
|
391
396
|
writeResponsesStreamStart(response, responseId, normalized.model, messageId);
|
|
397
|
+
writeResponsesReasoningPartAdded(response, responseId);
|
|
392
398
|
const heartbeat = startSseHeartbeat(response);
|
|
393
399
|
let text = "";
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
text
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
400
|
+
try {
|
|
401
|
+
const payload = await context.executeChatCompletion(chatPayload, {
|
|
402
|
+
timeoutMs: normalized.timeoutMs,
|
|
403
|
+
onTextDelta: (delta) => {
|
|
404
|
+
text += delta;
|
|
405
|
+
writeResponsesTextDelta(response, messageId, delta);
|
|
406
|
+
},
|
|
407
|
+
onTextDone: (doneText) => {
|
|
408
|
+
if (text.length === 0) {
|
|
409
|
+
text = doneText;
|
|
410
|
+
writeResponsesTextDelta(response, messageId, doneText);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
onRuntimeEvent: (event) => {
|
|
414
|
+
writeResponsesRuntimeEvent(response, responseId, event);
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
const outputText = text || getChatCompletionText(payload);
|
|
418
|
+
if (text.length === 0 && outputText.length > 0) {
|
|
419
|
+
writeResponsesTextDelta(response, messageId, outputText);
|
|
420
|
+
}
|
|
421
|
+
writeResponsesStreamDone(response, responseId, normalized.model, messageId, outputText);
|
|
422
|
+
response.end();
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
clearInterval(heartbeat);
|
|
414
426
|
}
|
|
415
|
-
writeResponsesStreamDone(response, responseId, normalized.model, messageId, outputText);
|
|
416
|
-
response.end();
|
|
417
427
|
return;
|
|
418
428
|
}
|
|
419
429
|
const payload = await context.executeChatCompletion(chatPayload, {
|
|
@@ -432,9 +442,15 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
432
442
|
response.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
433
443
|
}
|
|
434
444
|
catch (error) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
445
|
+
if (!response.headersSent) {
|
|
446
|
+
const statusCode = mapBridgeErrorStatus(error);
|
|
447
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
448
|
+
response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error), code: isCliError(error) ? error.code : "BRIDGE_RUNTIME_FAILURE" } }));
|
|
449
|
+
}
|
|
450
|
+
else if (!response.writableEnded) {
|
|
451
|
+
response.write(`data: ${JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error), code: isCliError(error) ? error.code : "BRIDGE_RUNTIME_FAILURE" } })}\n\n`);
|
|
452
|
+
response.end();
|
|
453
|
+
}
|
|
438
454
|
}
|
|
439
455
|
};
|
|
440
456
|
}
|
|
@@ -631,7 +647,7 @@ function writeResponsesStream(response, payload) {
|
|
|
631
647
|
});
|
|
632
648
|
writeSseEvent(response, "response.output_item.added", {
|
|
633
649
|
type: "response.output_item.added",
|
|
634
|
-
output_index:
|
|
650
|
+
output_index: 1,
|
|
635
651
|
item: {
|
|
636
652
|
id: messageId,
|
|
637
653
|
type: "message",
|
|
@@ -643,7 +659,7 @@ function writeResponsesStream(response, payload) {
|
|
|
643
659
|
writeSseEvent(response, "response.content_part.added", {
|
|
644
660
|
type: "response.content_part.added",
|
|
645
661
|
item_id: messageId,
|
|
646
|
-
output_index:
|
|
662
|
+
output_index: 1,
|
|
647
663
|
content_index: 0,
|
|
648
664
|
part: {
|
|
649
665
|
type: "output_text",
|
|
@@ -654,21 +670,21 @@ function writeResponsesStream(response, payload) {
|
|
|
654
670
|
writeSseEvent(response, "response.output_text.delta", {
|
|
655
671
|
type: "response.output_text.delta",
|
|
656
672
|
item_id: messageId,
|
|
657
|
-
output_index:
|
|
673
|
+
output_index: 1,
|
|
658
674
|
content_index: 0,
|
|
659
675
|
delta: outputText,
|
|
660
676
|
});
|
|
661
677
|
writeSseEvent(response, "response.output_text.done", {
|
|
662
678
|
type: "response.output_text.done",
|
|
663
679
|
item_id: messageId,
|
|
664
|
-
output_index:
|
|
680
|
+
output_index: 1,
|
|
665
681
|
content_index: 0,
|
|
666
682
|
text: outputText,
|
|
667
683
|
});
|
|
668
684
|
writeSseEvent(response, "response.content_part.done", {
|
|
669
685
|
type: "response.content_part.done",
|
|
670
686
|
item_id: messageId,
|
|
671
|
-
output_index:
|
|
687
|
+
output_index: 1,
|
|
672
688
|
content_index: 0,
|
|
673
689
|
part: {
|
|
674
690
|
type: "output_text",
|
|
@@ -678,7 +694,7 @@ function writeResponsesStream(response, payload) {
|
|
|
678
694
|
});
|
|
679
695
|
writeSseEvent(response, "response.output_item.done", {
|
|
680
696
|
type: "response.output_item.done",
|
|
681
|
-
output_index:
|
|
697
|
+
output_index: 1,
|
|
682
698
|
item: completedMessage,
|
|
683
699
|
});
|
|
684
700
|
writeSseEvent(response, "response.completed", {
|
|
@@ -710,7 +726,7 @@ function writeResponsesStreamStart(response, responseId, model, messageId) {
|
|
|
710
726
|
});
|
|
711
727
|
writeSseEvent(response, "response.output_item.added", {
|
|
712
728
|
type: "response.output_item.added",
|
|
713
|
-
output_index:
|
|
729
|
+
output_index: 1,
|
|
714
730
|
item: {
|
|
715
731
|
id: messageId,
|
|
716
732
|
type: "message",
|
|
@@ -722,7 +738,7 @@ function writeResponsesStreamStart(response, responseId, model, messageId) {
|
|
|
722
738
|
writeSseEvent(response, "response.content_part.added", {
|
|
723
739
|
type: "response.content_part.added",
|
|
724
740
|
item_id: messageId,
|
|
725
|
-
output_index:
|
|
741
|
+
output_index: 1,
|
|
726
742
|
content_index: 0,
|
|
727
743
|
part: {
|
|
728
744
|
type: "output_text",
|
|
@@ -738,7 +754,7 @@ function writeResponsesTextDelta(response, messageId, delta) {
|
|
|
738
754
|
writeSseEvent(response, "response.output_text.delta", {
|
|
739
755
|
type: "response.output_text.delta",
|
|
740
756
|
item_id: messageId,
|
|
741
|
-
output_index:
|
|
757
|
+
output_index: 1,
|
|
742
758
|
content_index: 0,
|
|
743
759
|
delta,
|
|
744
760
|
});
|
|
@@ -760,14 +776,14 @@ function writeResponsesStreamDone(response, responseId, model, messageId, output
|
|
|
760
776
|
writeSseEvent(response, "response.output_text.done", {
|
|
761
777
|
type: "response.output_text.done",
|
|
762
778
|
item_id: messageId,
|
|
763
|
-
output_index:
|
|
779
|
+
output_index: 1,
|
|
764
780
|
content_index: 0,
|
|
765
781
|
text: outputText,
|
|
766
782
|
});
|
|
767
783
|
writeSseEvent(response, "response.content_part.done", {
|
|
768
784
|
type: "response.content_part.done",
|
|
769
785
|
item_id: messageId,
|
|
770
|
-
output_index:
|
|
786
|
+
output_index: 1,
|
|
771
787
|
content_index: 0,
|
|
772
788
|
part: {
|
|
773
789
|
type: "output_text",
|
|
@@ -777,7 +793,7 @@ function writeResponsesStreamDone(response, responseId, model, messageId, output
|
|
|
777
793
|
});
|
|
778
794
|
writeSseEvent(response, "response.output_item.done", {
|
|
779
795
|
type: "response.output_item.done",
|
|
780
|
-
output_index:
|
|
796
|
+
output_index: 1,
|
|
781
797
|
item: completedMessage,
|
|
782
798
|
});
|
|
783
799
|
writeSseEvent(response, "response.completed", {
|
|
@@ -797,23 +813,40 @@ function writeResponsesRuntimeEvent(response, responseId, event) {
|
|
|
797
813
|
if (event.type === "assistant.message_delta") {
|
|
798
814
|
return;
|
|
799
815
|
}
|
|
800
|
-
if (event.type === "assistant.reasoning_delta") {
|
|
801
|
-
writeResponsesReasoningDelta(response, responseId, formatRuntimeEventText(event));
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
816
|
if (event.type === "session.unknown") {
|
|
805
|
-
process.stderr.write(`[${new Date().toISOString()}] bridge runtime event ignored type=${event.sdkType} summary=${truncateBridgeText(event.summary, 240)}\n`);
|
|
817
|
+
process.stderr.write(`[${new Date().toISOString()}] bridge runtime event ignored type=${event.sdkType ?? "unknown"} summary=${truncateBridgeText(event.summary ?? "", 240)}\n`);
|
|
806
818
|
return;
|
|
807
819
|
}
|
|
808
820
|
const text = formatRuntimeEventText(event);
|
|
809
821
|
if (text.length === 0) {
|
|
810
822
|
return;
|
|
811
823
|
}
|
|
824
|
+
writeResponsesReasoningDelta(response, responseId, text);
|
|
812
825
|
writeResponsesCommentaryItem(response, responseId, text);
|
|
813
826
|
process.stderr.write(`[${new Date().toISOString()}] bridge runtime event type=${event.type} summary=${truncateBridgeText(text, 240)}\n`);
|
|
814
827
|
}
|
|
815
828
|
function writeResponsesReasoningDelta(response, responseId, text) {
|
|
816
829
|
const reasoningId = `${responseId}_rs_0`;
|
|
830
|
+
writeSseEvent(response, "response.reasoning_summary_text.delta", {
|
|
831
|
+
type: "response.reasoning_summary_text.delta",
|
|
832
|
+
item_id: reasoningId,
|
|
833
|
+
output_index: 0,
|
|
834
|
+
summary_index: 0,
|
|
835
|
+
delta: text,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
function writeResponsesReasoningPartAdded(response, responseId) {
|
|
839
|
+
const reasoningId = `${responseId}_rs_0`;
|
|
840
|
+
writeSseEvent(response, "response.output_item.added", {
|
|
841
|
+
type: "response.output_item.added",
|
|
842
|
+
output_index: 0,
|
|
843
|
+
item: {
|
|
844
|
+
id: reasoningId,
|
|
845
|
+
type: "reasoning",
|
|
846
|
+
status: "in_progress",
|
|
847
|
+
summary: [],
|
|
848
|
+
},
|
|
849
|
+
});
|
|
817
850
|
writeSseEvent(response, "response.reasoning_summary_part.added", {
|
|
818
851
|
type: "response.reasoning_summary_part.added",
|
|
819
852
|
item_id: reasoningId,
|
|
@@ -824,19 +857,12 @@ function writeResponsesReasoningDelta(response, responseId, text) {
|
|
|
824
857
|
text: "",
|
|
825
858
|
},
|
|
826
859
|
});
|
|
827
|
-
writeSseEvent(response, "response.reasoning_summary_text.delta", {
|
|
828
|
-
type: "response.reasoning_summary_text.delta",
|
|
829
|
-
item_id: reasoningId,
|
|
830
|
-
output_index: 0,
|
|
831
|
-
summary_index: 0,
|
|
832
|
-
delta: text,
|
|
833
|
-
});
|
|
834
860
|
}
|
|
835
861
|
function writeResponsesCommentaryItem(response, responseId, text) {
|
|
836
862
|
const itemId = `${responseId}_commentary_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
|
837
863
|
writeSseEvent(response, "response.output_item.done", {
|
|
838
864
|
type: "response.output_item.done",
|
|
839
|
-
output_index:
|
|
865
|
+
output_index: 1,
|
|
840
866
|
item: {
|
|
841
867
|
id: itemId,
|
|
842
868
|
type: "message",
|
|
@@ -856,9 +882,9 @@ function writeResponsesCommentaryItem(response, responseId, text) {
|
|
|
856
882
|
function formatRuntimeEventText(event) {
|
|
857
883
|
switch (event.type) {
|
|
858
884
|
case "assistant.intent":
|
|
859
|
-
return truncateBridgeText(event.text, 600);
|
|
885
|
+
return truncateBridgeText(event.text ?? "", 600);
|
|
860
886
|
case "assistant.reasoning_delta":
|
|
861
|
-
return truncateBridgeText(event.text, 600);
|
|
887
|
+
return truncateBridgeText(event.text ?? "", 600);
|
|
862
888
|
case "tool.execution_start":
|
|
863
889
|
return truncateBridgeText(`Copilot started ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
864
890
|
case "tool.execution_progress":
|
|
@@ -878,10 +904,12 @@ function formatRuntimeEventText(event) {
|
|
|
878
904
|
case "session.error":
|
|
879
905
|
return truncateBridgeText(`Copilot session error${formatSummarySuffix(event.summary)}`, 600);
|
|
880
906
|
case "session.idle":
|
|
881
|
-
return truncateBridgeText(event.summary, 600);
|
|
907
|
+
return truncateBridgeText(event.summary ?? "", 600);
|
|
882
908
|
case "assistant.message_delta":
|
|
883
909
|
case "session.unknown":
|
|
884
910
|
return "";
|
|
911
|
+
default:
|
|
912
|
+
return "";
|
|
885
913
|
}
|
|
886
914
|
}
|
|
887
915
|
function formatToolName(name) {
|
|
@@ -891,7 +919,10 @@ function formatRequestId(requestId) {
|
|
|
891
919
|
return requestId ? ` (${requestId})` : "";
|
|
892
920
|
}
|
|
893
921
|
function formatSummarySuffix(summary) {
|
|
894
|
-
|
|
922
|
+
if (!summary || summary.length === 0) {
|
|
923
|
+
return "";
|
|
924
|
+
}
|
|
925
|
+
return `: ${summary}`;
|
|
895
926
|
}
|
|
896
927
|
function truncateBridgeText(value, maxLength) {
|
|
897
928
|
if (value.length <= maxLength) {
|
|
@@ -901,7 +932,9 @@ function truncateBridgeText(value, maxLength) {
|
|
|
901
932
|
}
|
|
902
933
|
function startSseHeartbeat(response) {
|
|
903
934
|
return setInterval(() => {
|
|
904
|
-
response.
|
|
935
|
+
if (!response.writableEnded) {
|
|
936
|
+
response.write(": keep-alive\n\n");
|
|
937
|
+
}
|
|
905
938
|
}, 15000);
|
|
906
939
|
}
|
|
907
940
|
function getChatCompletionText(payload) {
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const http = __importStar(require("node:http"));
|
|
37
|
+
const https = __importStar(require("node:https"));
|
|
38
|
+
const crypto = __importStar(require("node:crypto"));
|
|
39
|
+
const copilot_token_1 = require("./copilot-token");
|
|
40
|
+
let tokenManager = null;
|
|
41
|
+
function logWorkerEvent(message) {
|
|
42
|
+
process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
const provider = process.env.CODEX_SWITCH_BRIDGE_PROVIDER ?? "copilot";
|
|
46
|
+
const host = process.env.CODEX_SWITCH_BRIDGE_HOST ?? "127.0.0.1";
|
|
47
|
+
const port = Number(process.env.CODEX_SWITCH_BRIDGE_PORT ?? "41415");
|
|
48
|
+
const localApiKey = process.env.CODEX_SWITCH_BRIDGE_API_KEY ?? "";
|
|
49
|
+
const toolHomeDir = process.env.CODEX_SWITCH_TOOL_HOME_DIR || undefined;
|
|
50
|
+
const staticCopilotToken = process.env.CODEX_SWITCH_BRIDGE_COPILOT_TOKEN || undefined;
|
|
51
|
+
logWorkerEvent(`worker startup provider=${provider} host=${host} port=${String(port)}`);
|
|
52
|
+
if (staticCopilotToken) {
|
|
53
|
+
tokenManager = (0, copilot_token_1.createStaticTokenManager)(staticCopilotToken);
|
|
54
|
+
logWorkerEvent("copilot token acquired (static), api base: https://api.githubcopilot.com");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const githubPat = process.env.CODEX_SWITCH_GITHUB_TOKEN || (0, copilot_token_1.readGithubToken)(toolHomeDir);
|
|
58
|
+
if (!githubPat) {
|
|
59
|
+
throw new Error("No GitHub token found. Run `codexs login copilot` first.");
|
|
60
|
+
}
|
|
61
|
+
tokenManager = (0, copilot_token_1.createTokenManager)(githubPat);
|
|
62
|
+
await tokenManager.getToken();
|
|
63
|
+
logWorkerEvent(`copilot token acquired, api base: ${tokenManager.getApiBaseUrl()}`);
|
|
64
|
+
}
|
|
65
|
+
const server = http.createServer(async (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
await handleRequest(req, res, localApiKey);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!res.headersSent) {
|
|
71
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
72
|
+
res.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const stopWorker = () => {
|
|
77
|
+
logWorkerEvent(`worker shutdown provider=${provider}`);
|
|
78
|
+
tokenManager?.stop();
|
|
79
|
+
server.close();
|
|
80
|
+
process.exit(0);
|
|
81
|
+
};
|
|
82
|
+
process.once("SIGINT", stopWorker);
|
|
83
|
+
process.once("SIGTERM", stopWorker);
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
server.once("error", reject);
|
|
86
|
+
server.listen(port, host, () => {
|
|
87
|
+
server.off("error", reject);
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
logWorkerEvent(`worker ready provider=${provider} host=${host} port=${String(port)}`);
|
|
92
|
+
}
|
|
93
|
+
async function handleRequest(req, res, localApiKey) {
|
|
94
|
+
const method = req.method ?? "GET";
|
|
95
|
+
const url = req.url ?? "/";
|
|
96
|
+
if (method === "GET" && url === "/healthz") {
|
|
97
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
98
|
+
res.end(JSON.stringify({ ok: true }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!isAuthorized(req, localApiKey)) {
|
|
102
|
+
res.writeHead(401, { "content-type": "application/json" });
|
|
103
|
+
res.end(JSON.stringify({ error: { message: "Unauthorized" } }));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (method === "GET" && url === "/v1/models") {
|
|
107
|
+
await proxyGet("/models", res);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (method === "POST" && url === "/v1/chat/completions") {
|
|
111
|
+
await proxyPost("/chat/completions", req, res);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (method === "POST" && url === "/v1/responses") {
|
|
115
|
+
await proxyPost("/responses", req, res);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
119
|
+
res.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
120
|
+
}
|
|
121
|
+
async function proxyGet(upstreamPath, res) {
|
|
122
|
+
const copilotToken = await tokenManager.getToken();
|
|
123
|
+
const apiBase = tokenManager.getApiBaseUrl();
|
|
124
|
+
const targetUrl = new URL(upstreamPath, apiBase);
|
|
125
|
+
const headers = (0, copilot_token_1.getCopilotRequestHeaders)(copilotToken);
|
|
126
|
+
const upstreamRes = await httpsRequest({
|
|
127
|
+
method: "GET",
|
|
128
|
+
url: targetUrl,
|
|
129
|
+
headers,
|
|
130
|
+
});
|
|
131
|
+
res.writeHead(upstreamRes.statusCode, filterResponseHeaders(upstreamRes.headers));
|
|
132
|
+
upstreamRes.pipe(res);
|
|
133
|
+
}
|
|
134
|
+
async function proxyPost(upstreamPath, req, res) {
|
|
135
|
+
const body = await readRequestBody(req);
|
|
136
|
+
const copilotToken = await tokenManager.getToken();
|
|
137
|
+
const apiBase = tokenManager.getApiBaseUrl();
|
|
138
|
+
const targetUrl = new URL(upstreamPath, apiBase);
|
|
139
|
+
const requestId = crypto.randomUUID();
|
|
140
|
+
const headers = (0, copilot_token_1.getCopilotRequestHeaders)(copilotToken, requestId);
|
|
141
|
+
// Preserve content-length for the body
|
|
142
|
+
headers["content-length"] = Buffer.byteLength(body).toString();
|
|
143
|
+
const upstreamRes = await httpsRequest({
|
|
144
|
+
method: "POST",
|
|
145
|
+
url: targetUrl,
|
|
146
|
+
headers,
|
|
147
|
+
body,
|
|
148
|
+
});
|
|
149
|
+
// If upstream returned 401, try refreshing token once and retry
|
|
150
|
+
if (upstreamRes.statusCode === 401) {
|
|
151
|
+
upstreamRes.resume();
|
|
152
|
+
logWorkerEvent("upstream 401, invalidating token and retrying");
|
|
153
|
+
tokenManager.invalidate();
|
|
154
|
+
const freshToken = await tokenManager.getToken();
|
|
155
|
+
const retryHeaders = (0, copilot_token_1.getCopilotRequestHeaders)(freshToken, crypto.randomUUID());
|
|
156
|
+
retryHeaders["content-length"] = Buffer.byteLength(body).toString();
|
|
157
|
+
const retryRes = await httpsRequest({
|
|
158
|
+
method: "POST",
|
|
159
|
+
url: targetUrl,
|
|
160
|
+
headers: retryHeaders,
|
|
161
|
+
body,
|
|
162
|
+
});
|
|
163
|
+
res.writeHead(retryRes.statusCode, filterResponseHeaders(retryRes.headers));
|
|
164
|
+
retryRes.pipe(res);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
res.writeHead(upstreamRes.statusCode, filterResponseHeaders(upstreamRes.headers));
|
|
168
|
+
upstreamRes.pipe(res);
|
|
169
|
+
}
|
|
170
|
+
function httpsRequest(args) {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const req = https.request({
|
|
173
|
+
hostname: args.url.hostname,
|
|
174
|
+
port: args.url.port || 443,
|
|
175
|
+
path: args.url.pathname + args.url.search,
|
|
176
|
+
method: args.method,
|
|
177
|
+
headers: args.headers,
|
|
178
|
+
}, (res) => {
|
|
179
|
+
resolve(res);
|
|
180
|
+
});
|
|
181
|
+
req.on("error", reject);
|
|
182
|
+
if (args.body) {
|
|
183
|
+
req.write(args.body);
|
|
184
|
+
}
|
|
185
|
+
req.end();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function readRequestBody(req) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const chunks = [];
|
|
191
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
192
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
193
|
+
req.on("error", reject);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function filterResponseHeaders(headers) {
|
|
197
|
+
const filtered = {};
|
|
198
|
+
const passthrough = ["content-type", "transfer-encoding", "x-request-id"];
|
|
199
|
+
for (const key of passthrough) {
|
|
200
|
+
if (headers[key]) {
|
|
201
|
+
filtered[key] = headers[key];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Always allow cache-control for SSE
|
|
205
|
+
if (headers["cache-control"]) {
|
|
206
|
+
filtered["cache-control"] = headers["cache-control"];
|
|
207
|
+
}
|
|
208
|
+
return filtered;
|
|
209
|
+
}
|
|
210
|
+
function isAuthorized(req, expectedApiKey) {
|
|
211
|
+
const authorization = req.headers.authorization;
|
|
212
|
+
if (!authorization || !authorization.startsWith("Bearer ")) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return authorization.slice("Bearer ".length) === expectedApiKey;
|
|
216
|
+
}
|
|
217
|
+
if (require.main === module) {
|
|
218
|
+
process.on("uncaughtException", (error) => {
|
|
219
|
+
logWorkerEvent(`worker uncaught exception: ${error.message}`);
|
|
220
|
+
});
|
|
221
|
+
process.on("unhandledRejection", (reason) => {
|
|
222
|
+
logWorkerEvent(`worker unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
223
|
+
});
|
|
224
|
+
void main().catch((error) => {
|
|
225
|
+
logWorkerEvent(`worker startup failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
});
|
|
228
|
+
}
|