@minniexcode/codex-switch 0.0.7 → 0.0.8
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 +1 -1
- package/dist/app/add-provider.js +70 -3
- package/dist/app/get-status.js +24 -1
- package/dist/app/run-doctor.js +36 -1
- package/dist/app/setup-codex.js +2 -2
- package/dist/app/switch-provider.js +47 -1
- package/dist/cli.js +1 -1
- package/dist/commands/handlers.js +27 -2
- package/dist/commands/registry.js +14 -2
- package/dist/domain/providers.js +74 -0
- package/dist/runtime/copilot-adapter.js +173 -0
- package/dist/runtime/copilot-bridge-worker.js +25 -0
- package/dist/runtime/copilot-bridge.js +433 -0
- package/dist/runtime/copilot-installer.js +125 -0
- package/dist/runtime/copilot-sdk-loader.js +59 -0
- package/dist/storage/fs-utils.js +3 -0
- package/dist/storage/runtime-state-repo.js +80 -0
- package/docs/Design/codex-switch-v0.0.8-design.md +132 -0
- package/docs/Design/codex-switch-v0.0.9-to-v0.0.12-roadmap.md +413 -0
- package/docs/PRD/codex-switch-prd-v0.0.8.md +62 -0
- package/package.json +1 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.probeCopilotSdkRuntime = probeCopilotSdkRuntime;
|
|
4
|
+
exports.requireCopilotSdk = requireCopilotSdk;
|
|
5
|
+
exports.readCopilotAuthState = readCopilotAuthState;
|
|
6
|
+
exports.sendCopilotChatCompletion = sendCopilotChatCompletion;
|
|
7
|
+
const errors_1 = require("../domain/errors");
|
|
8
|
+
const copilot_sdk_loader_1 = require("./copilot-sdk-loader");
|
|
9
|
+
const copilot_installer_1 = require("./copilot-installer");
|
|
10
|
+
/**
|
|
11
|
+
* Probes whether the optional Copilot SDK runtime is installed and loadable.
|
|
12
|
+
*/
|
|
13
|
+
function probeCopilotSdkRuntime() {
|
|
14
|
+
const status = (0, copilot_installer_1.probeCopilotSdkInstall)();
|
|
15
|
+
if (!status.installed) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
runtime: "copilot-sdk",
|
|
19
|
+
reason: "missing",
|
|
20
|
+
cause: "The optional Copilot SDK runtime is not installed.",
|
|
21
|
+
details: {
|
|
22
|
+
installDir: status.installDir,
|
|
23
|
+
packageName: status.packageName,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
runtime: "copilot-sdk",
|
|
30
|
+
version: status.packageVersion ?? undefined,
|
|
31
|
+
details: {
|
|
32
|
+
installDir: status.installDir,
|
|
33
|
+
packageName: status.packageName,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Loads the lazily installed Copilot SDK and returns the module.
|
|
39
|
+
*/
|
|
40
|
+
async function requireCopilotSdk() {
|
|
41
|
+
return (0, copilot_sdk_loader_1.loadCopilotSdk)();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Probes whether the lazily installed Copilot SDK can create a usable session.
|
|
45
|
+
*/
|
|
46
|
+
async function readCopilotAuthState() {
|
|
47
|
+
const runtime = probeCopilotSdkRuntime();
|
|
48
|
+
if (!runtime.ok) {
|
|
49
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed.", runtime.details);
|
|
50
|
+
}
|
|
51
|
+
const { client, session } = await createCopilotSession();
|
|
52
|
+
await stopCopilotClient(client);
|
|
53
|
+
return {
|
|
54
|
+
ready: Boolean(session),
|
|
55
|
+
source: "official-sdk",
|
|
56
|
+
mode: "session",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Executes a single chat-completions style request through the optional Copilot SDK when available.
|
|
61
|
+
*/
|
|
62
|
+
async function sendCopilotChatCompletion(args) {
|
|
63
|
+
const { client, session, sdk } = await createCopilotSession();
|
|
64
|
+
try {
|
|
65
|
+
const sendAndWait = resolveCallable(session, "sendAndWait") ?? resolveCallable(sdk, "sendAndWait");
|
|
66
|
+
if (!sendAndWait) {
|
|
67
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK does not expose a supported sendAndWait API.", {
|
|
68
|
+
provider: args.provider,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const prompt = Array.isArray(args.payload.messages)
|
|
72
|
+
? args.payload.messages
|
|
73
|
+
.map((entry) => {
|
|
74
|
+
const message = entry;
|
|
75
|
+
return `${String(message.role ?? "user")}: ${String(message.content ?? "")}`;
|
|
76
|
+
})
|
|
77
|
+
.join("\n")
|
|
78
|
+
: "";
|
|
79
|
+
const result = await Promise.resolve(sendAndWait({ model: args.payload.model, prompt }));
|
|
80
|
+
const content = typeof result === "string"
|
|
81
|
+
? result
|
|
82
|
+
: typeof result?.content === "string"
|
|
83
|
+
? String(result.content)
|
|
84
|
+
: typeof result?.data === "object" &&
|
|
85
|
+
typeof result.data.content === "string"
|
|
86
|
+
? String(result.data.content)
|
|
87
|
+
: JSON.stringify(result);
|
|
88
|
+
return {
|
|
89
|
+
id: `copilot-${Date.now()}`,
|
|
90
|
+
object: "chat.completion",
|
|
91
|
+
created: Math.floor(Date.now() / 1000),
|
|
92
|
+
model: args.payload.model ?? "copilot",
|
|
93
|
+
choices: [
|
|
94
|
+
{
|
|
95
|
+
index: 0,
|
|
96
|
+
message: {
|
|
97
|
+
role: "assistant",
|
|
98
|
+
content,
|
|
99
|
+
},
|
|
100
|
+
finish_reason: "stop",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await stopCopilotClient(client);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function createCopilotSession() {
|
|
110
|
+
const sdk = (await requireCopilotSdk());
|
|
111
|
+
const client = createCopilotClient(sdk);
|
|
112
|
+
const createSession = resolveCallable(client ? client : null, "createSession") ?? resolveCallable(sdk, "createSession");
|
|
113
|
+
if (!createSession) {
|
|
114
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK does not expose a supported createSession API.", {});
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const session = (await Promise.resolve(createSession({})));
|
|
118
|
+
return {
|
|
119
|
+
sdk,
|
|
120
|
+
client,
|
|
121
|
+
session,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be used.", {
|
|
126
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function createCopilotClient(sdk) {
|
|
131
|
+
const ClientCtor = resolveConstructor(sdk, "CopilotClient");
|
|
132
|
+
if (!ClientCtor) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return new ClientCtor();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK CopilotClient could not be constructed.", {
|
|
140
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function stopCopilotClient(client) {
|
|
145
|
+
if (client && typeof client.stop === "function") {
|
|
146
|
+
await Promise.resolve(client.stop());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function resolveCallable(target, name) {
|
|
150
|
+
if (!target) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const direct = target[name];
|
|
154
|
+
if (typeof direct === "function") {
|
|
155
|
+
return direct;
|
|
156
|
+
}
|
|
157
|
+
const nestedDefault = target.default;
|
|
158
|
+
if (nestedDefault && typeof nestedDefault[name] === "function") {
|
|
159
|
+
return nestedDefault[name];
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function resolveConstructor(target, name) {
|
|
164
|
+
const direct = target[name];
|
|
165
|
+
if (typeof direct === "function") {
|
|
166
|
+
return direct;
|
|
167
|
+
}
|
|
168
|
+
const nestedDefault = target.default;
|
|
169
|
+
if (nestedDefault && typeof nestedDefault[name] === "function") {
|
|
170
|
+
return nestedDefault[name];
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const copilot_bridge_1 = require("./copilot-bridge");
|
|
4
|
+
const copilot_adapter_1 = require("./copilot-adapter");
|
|
5
|
+
async function main() {
|
|
6
|
+
const provider = process.env.CODEX_SWITCH_BRIDGE_PROVIDER ?? "copilot";
|
|
7
|
+
const host = process.env.CODEX_SWITCH_BRIDGE_HOST ?? "127.0.0.1";
|
|
8
|
+
const port = Number(process.env.CODEX_SWITCH_BRIDGE_PORT ?? "4141");
|
|
9
|
+
const apiKey = process.env.CODEX_SWITCH_BRIDGE_API_KEY ?? "";
|
|
10
|
+
await (0, copilot_bridge_1.startCopilotBridgeServer)({
|
|
11
|
+
host,
|
|
12
|
+
port,
|
|
13
|
+
apiKey,
|
|
14
|
+
executeChatCompletion: async (payload) => (0, copilot_adapter_1.sendCopilotChatCompletion)({
|
|
15
|
+
provider,
|
|
16
|
+
payload,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (require.main === module) {
|
|
21
|
+
void main().catch((error) => {
|
|
22
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,433 @@
|
|
|
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
|
+
exports.setCopilotBridgeSpawnImplementation = setCopilotBridgeSpawnImplementation;
|
|
37
|
+
exports.resetCopilotBridgeSpawnImplementation = resetCopilotBridgeSpawnImplementation;
|
|
38
|
+
exports.probeCopilotBridgeRuntime = probeCopilotBridgeRuntime;
|
|
39
|
+
exports.ensureCopilotBridge = ensureCopilotBridge;
|
|
40
|
+
exports.createCopilotBridgeRequestHandler = createCopilotBridgeRequestHandler;
|
|
41
|
+
exports.startCopilotBridgeServer = startCopilotBridgeServer;
|
|
42
|
+
exports.waitForCopilotBridgeHealth = waitForCopilotBridgeHealth;
|
|
43
|
+
exports.stopCopilotBridge = stopCopilotBridge;
|
|
44
|
+
const http = __importStar(require("node:http"));
|
|
45
|
+
const net = __importStar(require("node:net"));
|
|
46
|
+
const node_child_process_1 = require("node:child_process");
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const providers_1 = require("../domain/providers");
|
|
49
|
+
const errors_1 = require("../domain/errors");
|
|
50
|
+
const runtime_state_repo_1 = require("../storage/runtime-state-repo");
|
|
51
|
+
let spawnImplementation = node_child_process_1.spawn;
|
|
52
|
+
/**
|
|
53
|
+
* Overrides the spawn implementation for bridge runtime tests.
|
|
54
|
+
*/
|
|
55
|
+
function setCopilotBridgeSpawnImplementation(spawnLike) {
|
|
56
|
+
spawnImplementation = spawnLike;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Restores the default spawn implementation for bridge runtime tests.
|
|
60
|
+
*/
|
|
61
|
+
function resetCopilotBridgeSpawnImplementation() {
|
|
62
|
+
spawnImplementation = node_child_process_1.spawn;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Returns the last known Copilot bridge runtime status.
|
|
66
|
+
*/
|
|
67
|
+
async function probeCopilotBridgeRuntime(provider) {
|
|
68
|
+
const state = (0, runtime_state_repo_1.readCopilotBridgeState)();
|
|
69
|
+
if (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
runtime: "copilot-bridge",
|
|
73
|
+
reason: "missing",
|
|
74
|
+
cause: "No active Copilot bridge provider is selected.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const runtime = provider.runtime;
|
|
78
|
+
if (!runtime) {
|
|
79
|
+
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider runtime block is missing.", {
|
|
80
|
+
provider: state?.provider ?? null,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!state) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
runtime: "copilot-bridge",
|
|
87
|
+
reason: "missing",
|
|
88
|
+
cause: "Copilot bridge state manifest is missing.",
|
|
89
|
+
details: {
|
|
90
|
+
expectedBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (state.baseUrl !== (0, providers_1.buildCopilotBridgeBaseUrl)(runtime)) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
runtime: "copilot-bridge",
|
|
98
|
+
reason: "failed",
|
|
99
|
+
cause: "Copilot bridge state base URL does not match the provider runtime configuration.",
|
|
100
|
+
details: {
|
|
101
|
+
stateBaseUrl: state.baseUrl,
|
|
102
|
+
providerBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const healthy = await healthcheckCopilotBridge(state.host, state.port);
|
|
107
|
+
if (!healthy.ok) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
runtime: "copilot-bridge",
|
|
111
|
+
reason: "failed",
|
|
112
|
+
cause: healthy.cause,
|
|
113
|
+
details: state,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
(0, runtime_state_repo_1.writeCopilotBridgeState)({
|
|
117
|
+
...state,
|
|
118
|
+
lastHealthcheckAt: new Date().toISOString(),
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
runtime: "copilot-bridge",
|
|
123
|
+
details: state,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Starts or reuses a Copilot bridge worker, then verifies its health before returning.
|
|
128
|
+
*/
|
|
129
|
+
async function ensureCopilotBridge(providerName, provider) {
|
|
130
|
+
if (!(0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
131
|
+
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider is not backed by a Copilot bridge runtime.", {
|
|
132
|
+
provider: providerName,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const runtime = provider.runtime;
|
|
136
|
+
if (!runtime) {
|
|
137
|
+
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider runtime block is missing.", {
|
|
138
|
+
provider: providerName,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const expectedBaseUrl = (0, providers_1.buildCopilotBridgeBaseUrl)(runtime);
|
|
142
|
+
const current = (0, runtime_state_repo_1.readCopilotBridgeState)();
|
|
143
|
+
if (current && current.provider === providerName && current.baseUrl === expectedBaseUrl) {
|
|
144
|
+
const healthy = await healthcheckCopilotBridge(current.host, current.port);
|
|
145
|
+
if (healthy.ok) {
|
|
146
|
+
(0, runtime_state_repo_1.writeCopilotBridgeState)({
|
|
147
|
+
...current,
|
|
148
|
+
lastHealthcheckAt: new Date().toISOString(),
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
baseUrl: expectedBaseUrl,
|
|
152
|
+
reused: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const portCheck = await checkPortAvailability(runtime.bridgeHost, runtime.bridgePort);
|
|
157
|
+
if (!portCheck.ok) {
|
|
158
|
+
throw (0, errors_1.cliError)("BRIDGE_PORT_CONFLICT", "Copilot bridge port is already in use.", {
|
|
159
|
+
provider: providerName,
|
|
160
|
+
host: runtime.bridgeHost,
|
|
161
|
+
port: runtime.bridgePort,
|
|
162
|
+
cause: portCheck.cause,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
|
|
166
|
+
let child;
|
|
167
|
+
try {
|
|
168
|
+
child = spawnImplementation(process.execPath, [workerPath], {
|
|
169
|
+
detached: true,
|
|
170
|
+
stdio: "ignore",
|
|
171
|
+
env: {
|
|
172
|
+
...process.env,
|
|
173
|
+
CODEX_SWITCH_BRIDGE_PROVIDER: providerName,
|
|
174
|
+
CODEX_SWITCH_BRIDGE_HOST: runtime.bridgeHost,
|
|
175
|
+
CODEX_SWITCH_BRIDGE_PORT: String(runtime.bridgePort),
|
|
176
|
+
CODEX_SWITCH_BRIDGE_API_KEY: provider.apiKey,
|
|
177
|
+
CODEX_SWITCH_BRIDGE_BASE_URL: expectedBaseUrl,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Failed to start the Copilot bridge worker.", {
|
|
183
|
+
provider: providerName,
|
|
184
|
+
host: runtime.bridgeHost,
|
|
185
|
+
port: runtime.bridgePort,
|
|
186
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
child.unref();
|
|
190
|
+
const startedAt = new Date().toISOString();
|
|
191
|
+
const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, runtime.bridgePort, 15, 200);
|
|
192
|
+
if (!healthy.ok) {
|
|
193
|
+
(0, runtime_state_repo_1.clearCopilotBridgeState)();
|
|
194
|
+
if (healthy.reason === "start-failed") {
|
|
195
|
+
throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
|
|
196
|
+
provider: providerName,
|
|
197
|
+
host: runtime.bridgeHost,
|
|
198
|
+
port: runtime.bridgePort,
|
|
199
|
+
cause: healthy.cause,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
throw (0, errors_1.cliError)("BRIDGE_HEALTHCHECK_FAILED", "Copilot bridge did not become healthy after startup.", {
|
|
203
|
+
provider: providerName,
|
|
204
|
+
host: runtime.bridgeHost,
|
|
205
|
+
port: runtime.bridgePort,
|
|
206
|
+
cause: healthy.cause,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const state = {
|
|
210
|
+
provider: providerName,
|
|
211
|
+
pid: child.pid ?? null,
|
|
212
|
+
host: runtime.bridgeHost,
|
|
213
|
+
port: runtime.bridgePort,
|
|
214
|
+
baseUrl: expectedBaseUrl,
|
|
215
|
+
startedAt,
|
|
216
|
+
lastHealthcheckAt: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
(0, runtime_state_repo_1.writeCopilotBridgeState)(state);
|
|
219
|
+
return {
|
|
220
|
+
baseUrl: expectedBaseUrl,
|
|
221
|
+
reused: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Creates an HTTP request handler implementing the minimal OpenAI-compatible bridge contract.
|
|
226
|
+
*/
|
|
227
|
+
function createCopilotBridgeRequestHandler(context) {
|
|
228
|
+
return async (request, response) => {
|
|
229
|
+
try {
|
|
230
|
+
const method = request.method ?? "GET";
|
|
231
|
+
const url = request.url ?? "/";
|
|
232
|
+
if (method === "GET" && url === "/healthz") {
|
|
233
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
234
|
+
response.end(JSON.stringify({ ok: true }));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!isAuthorized(request, context.apiKey)) {
|
|
238
|
+
response.writeHead(401, { "content-type": "application/json" });
|
|
239
|
+
response.end(JSON.stringify({ error: { message: "Unauthorized" } }));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (method === "GET" && url === "/v1/models") {
|
|
243
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
244
|
+
response.end(JSON.stringify({ object: "list", data: [] }));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (method !== "POST" || url !== "/v1/chat/completions") {
|
|
248
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
249
|
+
response.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const body = await readJsonBody(request);
|
|
253
|
+
const stream = Boolean(body.stream);
|
|
254
|
+
const payload = await context.executeChatCompletion(body);
|
|
255
|
+
if (stream) {
|
|
256
|
+
response.writeHead(200, {
|
|
257
|
+
"content-type": "text/event-stream",
|
|
258
|
+
"cache-control": "no-cache",
|
|
259
|
+
connection: "keep-alive",
|
|
260
|
+
});
|
|
261
|
+
response.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
262
|
+
response.write("data: [DONE]\n\n");
|
|
263
|
+
response.end();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
267
|
+
response.end(JSON.stringify(payload));
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
response.writeHead(500, { "content-type": "application/json" });
|
|
271
|
+
response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Starts an in-process local bridge server. Primarily used by the worker entrypoint and tests.
|
|
277
|
+
*/
|
|
278
|
+
function startCopilotBridgeServer(args) {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const server = http.createServer(createCopilotBridgeRequestHandler({
|
|
281
|
+
apiKey: args.apiKey,
|
|
282
|
+
executeChatCompletion: args.executeChatCompletion,
|
|
283
|
+
}));
|
|
284
|
+
server.once("error", reject);
|
|
285
|
+
server.listen(args.port, args.host, () => {
|
|
286
|
+
server.off("error", reject);
|
|
287
|
+
resolve(server);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Polls the bridge health endpoint until it becomes available or the retry budget is exhausted.
|
|
293
|
+
*/
|
|
294
|
+
async function waitForCopilotBridgeHealth(host, port, attempts = 10, delayMs = 150) {
|
|
295
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
296
|
+
const result = await healthcheckCopilotBridge(host, port);
|
|
297
|
+
if (result.ok) {
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
cause: "Timed out waiting for Copilot bridge health endpoint.",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Stops the currently persisted Copilot bridge worker when possible.
|
|
309
|
+
*/
|
|
310
|
+
function stopCopilotBridge() {
|
|
311
|
+
const state = (0, runtime_state_repo_1.readCopilotBridgeState)();
|
|
312
|
+
if (state?.pid) {
|
|
313
|
+
try {
|
|
314
|
+
process.kill(state.pid);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Ignore best-effort bridge cleanup failures.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
(0, runtime_state_repo_1.clearCopilotBridgeState)();
|
|
321
|
+
}
|
|
322
|
+
async function checkPortAvailability(host, port) {
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
const server = net.createServer();
|
|
325
|
+
server.once("error", (error) => {
|
|
326
|
+
resolve({
|
|
327
|
+
ok: false,
|
|
328
|
+
cause: error.message,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
server.listen(port, host, () => {
|
|
332
|
+
server.close((error) => {
|
|
333
|
+
if (error) {
|
|
334
|
+
resolve({
|
|
335
|
+
ok: false,
|
|
336
|
+
cause: error.message,
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
resolve({ ok: true });
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs) {
|
|
346
|
+
let startupFailure = null;
|
|
347
|
+
const onError = (error) => {
|
|
348
|
+
startupFailure = error.message;
|
|
349
|
+
};
|
|
350
|
+
const onExit = (code, signal) => {
|
|
351
|
+
startupFailure = `Worker exited with code ${String(code)} signal ${String(signal)}.`;
|
|
352
|
+
};
|
|
353
|
+
child.once("error", onError);
|
|
354
|
+
child.once("exit", onExit);
|
|
355
|
+
try {
|
|
356
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
357
|
+
if (startupFailure !== null) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
reason: "start-failed",
|
|
361
|
+
cause: startupFailure,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const result = await healthcheckCopilotBridge(host, port);
|
|
365
|
+
if (result.ok) {
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
369
|
+
}
|
|
370
|
+
if (startupFailure !== null) {
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
reason: "start-failed",
|
|
374
|
+
cause: startupFailure,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
reason: "healthcheck-failed",
|
|
380
|
+
cause: "Timed out waiting for Copilot bridge health endpoint.",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
child.off("error", onError);
|
|
385
|
+
child.off("exit", onExit);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function healthcheckCopilotBridge(host, port) {
|
|
389
|
+
return new Promise((resolve) => {
|
|
390
|
+
const request = http.request({
|
|
391
|
+
host,
|
|
392
|
+
port,
|
|
393
|
+
method: "GET",
|
|
394
|
+
path: "/healthz",
|
|
395
|
+
timeout: 1000,
|
|
396
|
+
}, (response) => {
|
|
397
|
+
response.resume();
|
|
398
|
+
if (response.statusCode === 200) {
|
|
399
|
+
resolve({ ok: true });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
resolve({
|
|
403
|
+
ok: false,
|
|
404
|
+
cause: `Health endpoint returned status ${String(response.statusCode ?? 0)}.`,
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
request.on("error", (error) => {
|
|
408
|
+
resolve({
|
|
409
|
+
ok: false,
|
|
410
|
+
cause: error.message,
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
request.on("timeout", () => {
|
|
414
|
+
request.destroy(new Error("Health endpoint timed out."));
|
|
415
|
+
});
|
|
416
|
+
request.end();
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async function readJsonBody(request) {
|
|
420
|
+
const chunks = [];
|
|
421
|
+
for await (const chunk of request) {
|
|
422
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
423
|
+
}
|
|
424
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
425
|
+
return raw.trim() === "" ? {} : JSON.parse(raw);
|
|
426
|
+
}
|
|
427
|
+
function isAuthorized(request, expectedApiKey) {
|
|
428
|
+
const authorization = request.headers.authorization;
|
|
429
|
+
if (!authorization || !authorization.startsWith("Bearer ")) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
return authorization.slice("Bearer ".length) === expectedApiKey;
|
|
433
|
+
}
|