@minniexcode/codex-switch 0.1.5 → 0.2.2
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.AI.md +66 -94
- package/README.CN.md +84 -139
- package/README.md +91 -151
- package/dist/app/add-provider.js +6 -89
- package/dist/app/edit-provider.js +9 -29
- package/dist/app/get-status.js +1 -74
- package/dist/app/list-providers.js +0 -2
- package/dist/app/remove-provider.js +3 -5
- package/dist/app/run-doctor.js +2 -100
- package/dist/app/setup-codex.js +0 -2
- package/dist/app/switch-provider.js +1 -79
- package/dist/cli/output.js +3 -89
- package/dist/commands/handlers.js +20 -209
- package/dist/commands/help.js +1 -4
- package/dist/commands/registry.js +6 -74
- package/dist/domain/config.js +1 -3
- package/dist/domain/providers.js +4 -92
- package/dist/domain/runtime-state.js +0 -88
- package/dist/interaction/add-interactive.js +1 -55
- package/dist/interaction/interactive.js +1 -3
- package/dist/runtime/codex-probe.js +0 -12
- package/dist/storage/codex-paths.js +0 -2
- package/docs/Design/codex-switch-v0.2.0-design.md +56 -0
- package/docs/Design/codex-switch-v0.2.1-design.md +77 -0
- package/docs/PRD/codex-switch-prd-v0.2.1.md +82 -0
- package/docs/Tests/testing.md +32 -34
- package/docs/cli-usage.md +67 -235
- package/docs/codex-switch-command-design.md +1 -1
- package/docs/codex-switch-product-overview.md +49 -96
- package/docs/codex-switch-technical-architecture.md +37 -52
- package/package.json +1 -1
- package/dist/app/bridge.js +0 -308
- package/dist/runtime/copilot-adapter.js +0 -612
- package/dist/runtime/copilot-bridge-worker.js +0 -69
- package/dist/runtime/copilot-bridge.js +0 -1318
- package/dist/runtime/copilot-cli.js +0 -164
- package/dist/runtime/copilot-installer.js +0 -231
- package/dist/runtime/copilot-sdk-loader.js +0 -62
- package/dist/storage/runtime-state-repo.js +0 -121
|
@@ -1,1318 +0,0 @@
|
|
|
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.startOrReuseCopilotBridge = startOrReuseCopilotBridge;
|
|
41
|
-
exports.createCopilotBridgeRequestHandler = createCopilotBridgeRequestHandler;
|
|
42
|
-
exports.startCopilotBridgeServer = startCopilotBridgeServer;
|
|
43
|
-
exports.waitForCopilotBridgeHealth = waitForCopilotBridgeHealth;
|
|
44
|
-
exports.stopCopilotBridge = stopCopilotBridge;
|
|
45
|
-
const http = __importStar(require("node:http"));
|
|
46
|
-
const net = __importStar(require("node:net"));
|
|
47
|
-
const node_child_process_1 = require("node:child_process");
|
|
48
|
-
const fs = __importStar(require("node:fs"));
|
|
49
|
-
const path = __importStar(require("node:path"));
|
|
50
|
-
const providers_1 = require("../domain/providers");
|
|
51
|
-
const errors_1 = require("../domain/errors");
|
|
52
|
-
const runtime_state_repo_1 = require("../storage/runtime-state-repo");
|
|
53
|
-
const BRIDGE_REUSE_ATTEMPTS = 2;
|
|
54
|
-
const BRIDGE_REUSE_TIMEOUT_MS = 2500;
|
|
55
|
-
const BRIDGE_REUSE_DELAY_MS = 250;
|
|
56
|
-
let spawnImplementation = node_child_process_1.spawn;
|
|
57
|
-
let cachedBridgeWorkerBuildId = null;
|
|
58
|
-
/**
|
|
59
|
-
* Overrides the spawn implementation for bridge runtime tests.
|
|
60
|
-
*/
|
|
61
|
-
function setCopilotBridgeSpawnImplementation(spawnLike) {
|
|
62
|
-
spawnImplementation = spawnLike;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Restores the default spawn implementation for bridge runtime tests.
|
|
66
|
-
*/
|
|
67
|
-
function resetCopilotBridgeSpawnImplementation() {
|
|
68
|
-
spawnImplementation = node_child_process_1.spawn;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Returns the last known Copilot bridge runtime status.
|
|
72
|
-
*/
|
|
73
|
-
async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
|
|
74
|
-
const state = persistedState === undefined ? (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir) : persistedState;
|
|
75
|
-
const logPath = state?.logPath ?? (0, runtime_state_repo_1.getCopilotBridgeLogPath)(runtimeDir);
|
|
76
|
-
if (state && (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider))) {
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
runtime: "copilot-bridge",
|
|
80
|
-
reason: "failed",
|
|
81
|
-
cause: "Copilot bridge runtime state exists but no active Copilot bridge provider is selected.",
|
|
82
|
-
details: {
|
|
83
|
-
...state,
|
|
84
|
-
logPath,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
if (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
runtime: "copilot-bridge",
|
|
92
|
-
reason: "missing",
|
|
93
|
-
cause: "No active Copilot bridge provider is selected.",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const runtime = provider.runtime;
|
|
97
|
-
if (!runtime) {
|
|
98
|
-
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider runtime block is missing.", {
|
|
99
|
-
provider: state?.provider ?? null,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (!state) {
|
|
103
|
-
return {
|
|
104
|
-
ok: false,
|
|
105
|
-
runtime: "copilot-bridge",
|
|
106
|
-
reason: "missing",
|
|
107
|
-
cause: "Copilot bridge state manifest is missing.",
|
|
108
|
-
details: {
|
|
109
|
-
expectedBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
|
|
110
|
-
logPath,
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
if (state.baseUrl !== (0, providers_1.buildCopilotBridgeBaseUrl)(runtime)) {
|
|
115
|
-
return {
|
|
116
|
-
ok: false,
|
|
117
|
-
runtime: "copilot-bridge",
|
|
118
|
-
reason: "failed",
|
|
119
|
-
cause: "Copilot bridge state base URL does not match the provider runtime configuration.",
|
|
120
|
-
details: {
|
|
121
|
-
stateBaseUrl: state.baseUrl,
|
|
122
|
-
providerBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
|
|
123
|
-
logPath,
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
const healthy = await probeBridgeEndpoint({
|
|
128
|
-
host: state.host,
|
|
129
|
-
port: state.port,
|
|
130
|
-
stage: "health",
|
|
131
|
-
});
|
|
132
|
-
if (!healthy.ok) {
|
|
133
|
-
return {
|
|
134
|
-
ok: false,
|
|
135
|
-
runtime: "copilot-bridge",
|
|
136
|
-
reason: "failed",
|
|
137
|
-
cause: healthy.message,
|
|
138
|
-
details: {
|
|
139
|
-
...state,
|
|
140
|
-
logPath,
|
|
141
|
-
probeStage: healthy.stage,
|
|
142
|
-
probeCause: healthy.cause,
|
|
143
|
-
retryable: healthy.retryable,
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
(0, runtime_state_repo_1.writeCopilotBridgeState)({
|
|
148
|
-
...state,
|
|
149
|
-
lastHealthcheckAt: new Date().toISOString(),
|
|
150
|
-
lastProbeAt: new Date().toISOString(),
|
|
151
|
-
logPath,
|
|
152
|
-
}, runtimeDir);
|
|
153
|
-
return {
|
|
154
|
-
ok: true,
|
|
155
|
-
runtime: "copilot-bridge",
|
|
156
|
-
details: {
|
|
157
|
-
...state,
|
|
158
|
-
logPath,
|
|
159
|
-
lastProbeAt: new Date().toISOString(),
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Starts or reuses a Copilot bridge worker, then verifies its health before returning.
|
|
165
|
-
*/
|
|
166
|
-
async function ensureCopilotBridge(providerName, provider, runtimeDir, runtimesDir) {
|
|
167
|
-
return startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir);
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Starts or reuses a Copilot bridge worker and reports the chosen port.
|
|
171
|
-
*/
|
|
172
|
-
async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir) {
|
|
173
|
-
if (!(0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
174
|
-
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider is not backed by a Copilot bridge runtime.", {
|
|
175
|
-
provider: providerName,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
const runtime = provider.runtime;
|
|
179
|
-
if (!runtime) {
|
|
180
|
-
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider runtime block is missing.", {
|
|
181
|
-
provider: providerName,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
const expectedBaseUrl = (0, providers_1.buildCopilotBridgeBaseUrl)(runtime);
|
|
185
|
-
const current = (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir);
|
|
186
|
-
const workerBuildId = getCopilotBridgeWorkerBuildId();
|
|
187
|
-
let replaced = false;
|
|
188
|
-
const logPath = current?.logPath ?? (0, runtime_state_repo_1.getCopilotBridgeLogPath)(runtimeDir);
|
|
189
|
-
const reuseDecision = await evaluateBridgeReuse({
|
|
190
|
-
current,
|
|
191
|
-
providerName,
|
|
192
|
-
expectedBaseUrl,
|
|
193
|
-
expectedApiKey: provider.apiKey,
|
|
194
|
-
workerBuildId,
|
|
195
|
-
runtimeDir,
|
|
196
|
-
logPath,
|
|
197
|
-
});
|
|
198
|
-
if (reuseDecision.reuse) {
|
|
199
|
-
(0, runtime_state_repo_1.writeCopilotBridgeState)({
|
|
200
|
-
...current,
|
|
201
|
-
lastHealthcheckAt: new Date().toISOString(),
|
|
202
|
-
lastProbeAt: reuseDecision.probeAt,
|
|
203
|
-
workerBuildId,
|
|
204
|
-
logPath: reuseDecision.logPath,
|
|
205
|
-
}, runtimeDir);
|
|
206
|
-
appendBridgeLifecycleLog(logPath, `startup success reused provider=${providerName} host=${current.host} port=${String(current.port)}`);
|
|
207
|
-
return {
|
|
208
|
-
baseUrl: expectedBaseUrl,
|
|
209
|
-
host: current.host,
|
|
210
|
-
port: current.port,
|
|
211
|
-
reused: true,
|
|
212
|
-
portChanged: false,
|
|
213
|
-
replaced: false,
|
|
214
|
-
logPath: reuseDecision.logPath,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
if (reuseDecision.replacedExisting) {
|
|
218
|
-
stopCopilotBridge(runtimeDir);
|
|
219
|
-
replaced = true;
|
|
220
|
-
appendBridgeLifecycleLog(logPath, `replacement reason=${reuseDecision.reason}`);
|
|
221
|
-
}
|
|
222
|
-
const selectedPort = await selectBridgePort(runtime.bridgeHost, runtime.bridgePort);
|
|
223
|
-
const selectedBaseUrl = `http://${runtime.bridgeHost}:${selectedPort}${runtime.bridgePath}`;
|
|
224
|
-
const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
|
|
225
|
-
ensureBridgeLogFile(logPath);
|
|
226
|
-
appendBridgeLifecycleLog(logPath, `worker start provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)} replaced=${String(replaced)}`);
|
|
227
|
-
let child;
|
|
228
|
-
try {
|
|
229
|
-
child = spawnImplementation(process.execPath, [workerPath], {
|
|
230
|
-
detached: true,
|
|
231
|
-
stdio: ["ignore", openBridgeLogFd(logPath), openBridgeLogFd(logPath)],
|
|
232
|
-
env: {
|
|
233
|
-
...process.env,
|
|
234
|
-
CODEX_SWITCH_BRIDGE_PROVIDER: providerName,
|
|
235
|
-
CODEX_SWITCH_BRIDGE_HOST: runtime.bridgeHost,
|
|
236
|
-
CODEX_SWITCH_BRIDGE_PORT: String(selectedPort),
|
|
237
|
-
CODEX_SWITCH_BRIDGE_API_KEY: provider.apiKey,
|
|
238
|
-
CODEX_SWITCH_BRIDGE_BASE_URL: selectedBaseUrl,
|
|
239
|
-
CODEX_SWITCH_RUNTIME_DIR: runtimeDir ?? "",
|
|
240
|
-
CODEX_SWITCH_RUNTIMES_DIR: runtimesDir ?? "",
|
|
241
|
-
CODEX_SWITCH_BRIDGE_LOG_PATH: logPath,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Failed to start the Copilot bridge worker.", {
|
|
247
|
-
provider: providerName,
|
|
248
|
-
host: runtime.bridgeHost,
|
|
249
|
-
port: selectedPort,
|
|
250
|
-
logPath,
|
|
251
|
-
probeStage: "startup",
|
|
252
|
-
probeCause: "startup-failed",
|
|
253
|
-
retryable: false,
|
|
254
|
-
replacedExisting: replaced,
|
|
255
|
-
providerName,
|
|
256
|
-
cause: error instanceof Error ? error.message : String(error),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
child.unref();
|
|
260
|
-
const startedAt = new Date().toISOString();
|
|
261
|
-
// The worker can take a little longer to become healthy on Windows or under loaded test runs.
|
|
262
|
-
const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, selectedPort, 25, 200);
|
|
263
|
-
if (!healthy.ok) {
|
|
264
|
-
(0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
|
|
265
|
-
if (healthy.reason === "start-failed") {
|
|
266
|
-
appendBridgeLifecycleLog(logPath, `startup failure provider=${providerName} cause=${healthy.cause}`);
|
|
267
|
-
throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
|
|
268
|
-
provider: providerName,
|
|
269
|
-
host: runtime.bridgeHost,
|
|
270
|
-
port: selectedPort,
|
|
271
|
-
logPath,
|
|
272
|
-
probeStage: "startup",
|
|
273
|
-
probeCause: "startup-failed",
|
|
274
|
-
retryable: false,
|
|
275
|
-
replacedExisting: replaced,
|
|
276
|
-
cause: healthy.cause,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
appendBridgeLifecycleLog(logPath, `startup timeout provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)}`);
|
|
280
|
-
throw (0, errors_1.cliError)("BRIDGE_HEALTHCHECK_FAILED", "Copilot bridge did not become healthy after startup.", {
|
|
281
|
-
provider: providerName,
|
|
282
|
-
host: runtime.bridgeHost,
|
|
283
|
-
port: selectedPort,
|
|
284
|
-
logPath,
|
|
285
|
-
probeStage: "startup",
|
|
286
|
-
probeCause: "startup-timeout",
|
|
287
|
-
retryable: true,
|
|
288
|
-
replacedExisting: replaced,
|
|
289
|
-
cause: healthy.cause,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
const state = {
|
|
293
|
-
provider: providerName,
|
|
294
|
-
pid: child.pid ?? null,
|
|
295
|
-
host: runtime.bridgeHost,
|
|
296
|
-
port: selectedPort,
|
|
297
|
-
baseUrl: selectedBaseUrl,
|
|
298
|
-
startedAt,
|
|
299
|
-
lastHealthcheckAt: new Date().toISOString(),
|
|
300
|
-
workerBuildId,
|
|
301
|
-
logPath,
|
|
302
|
-
lastProbeAt: new Date().toISOString(),
|
|
303
|
-
lastRestartReason: reuseDecision.reuse ? undefined : reuseDecision.reason,
|
|
304
|
-
};
|
|
305
|
-
(0, runtime_state_repo_1.writeCopilotBridgeState)(state, runtimeDir);
|
|
306
|
-
appendBridgeLifecycleLog(logPath, `startup success provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)}`);
|
|
307
|
-
return {
|
|
308
|
-
baseUrl: selectedBaseUrl,
|
|
309
|
-
host: runtime.bridgeHost,
|
|
310
|
-
port: selectedPort,
|
|
311
|
-
reused: false,
|
|
312
|
-
portChanged: selectedPort !== runtime.bridgePort,
|
|
313
|
-
replaced,
|
|
314
|
-
logPath,
|
|
315
|
-
restartReason: replaced ? reuseDecision.reason : undefined,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Creates an HTTP request handler implementing the minimal OpenAI-compatible bridge contract.
|
|
320
|
-
*/
|
|
321
|
-
function createCopilotBridgeRequestHandler(context) {
|
|
322
|
-
return async (request, response) => {
|
|
323
|
-
try {
|
|
324
|
-
const method = request.method ?? "GET";
|
|
325
|
-
const url = request.url ?? "/";
|
|
326
|
-
if (method === "GET" && url === "/healthz") {
|
|
327
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
328
|
-
response.end(JSON.stringify({ ok: true }));
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (!isAuthorized(request, context.apiKey)) {
|
|
332
|
-
response.writeHead(401, { "content-type": "application/json" });
|
|
333
|
-
response.end(JSON.stringify({ error: { message: "Unauthorized" } }));
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (method === "GET" && url === "/v1/models") {
|
|
337
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
338
|
-
response.end(JSON.stringify({ object: "list", data: [] }));
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (method === "POST" && url === "/v1/chat/completions") {
|
|
342
|
-
const body = await readJsonBody(request);
|
|
343
|
-
const timeoutMs = parseBridgeRequestTimeoutMs(body, "/v1/chat/completions");
|
|
344
|
-
const stream = Boolean(body.stream);
|
|
345
|
-
if (stream) {
|
|
346
|
-
response.writeHead(200, {
|
|
347
|
-
"content-type": "text/event-stream",
|
|
348
|
-
"cache-control": "no-cache",
|
|
349
|
-
connection: "keep-alive",
|
|
350
|
-
});
|
|
351
|
-
const heartbeat = startSseHeartbeat(response);
|
|
352
|
-
const payload = await context.executeChatCompletion(body, {
|
|
353
|
-
timeoutMs,
|
|
354
|
-
onTextDelta: (delta) => {
|
|
355
|
-
response.write(`data: ${JSON.stringify({
|
|
356
|
-
choices: [
|
|
357
|
-
{
|
|
358
|
-
index: 0,
|
|
359
|
-
delta: { content: delta },
|
|
360
|
-
finish_reason: null,
|
|
361
|
-
},
|
|
362
|
-
],
|
|
363
|
-
})}\n\n`);
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
clearInterval(heartbeat);
|
|
367
|
-
response.write("data: [DONE]\n\n");
|
|
368
|
-
response.end();
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const payload = await context.executeChatCompletion(body, { timeoutMs });
|
|
372
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
373
|
-
response.end(JSON.stringify(payload));
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (method === "POST" && url === "/v1/responses") {
|
|
377
|
-
const body = await readJsonBody(request);
|
|
378
|
-
const normalized = normalizeResponsesRequest(body);
|
|
379
|
-
const chatPayload = {
|
|
380
|
-
model: normalized.model,
|
|
381
|
-
messages: normalized.messages,
|
|
382
|
-
};
|
|
383
|
-
if (normalized.stream) {
|
|
384
|
-
response.writeHead(200, {
|
|
385
|
-
"content-type": "text/event-stream",
|
|
386
|
-
"cache-control": "no-cache",
|
|
387
|
-
connection: "keep-alive",
|
|
388
|
-
});
|
|
389
|
-
const responseId = `resp_${Date.now()}`;
|
|
390
|
-
const messageId = buildResponsesMessageId(responseId);
|
|
391
|
-
writeResponsesStreamStart(response, responseId, normalized.model, messageId);
|
|
392
|
-
const heartbeat = startSseHeartbeat(response);
|
|
393
|
-
let text = "";
|
|
394
|
-
const payload = await context.executeChatCompletion(chatPayload, {
|
|
395
|
-
timeoutMs: normalized.timeoutMs,
|
|
396
|
-
onTextDelta: (delta) => {
|
|
397
|
-
text += delta;
|
|
398
|
-
writeResponsesTextDelta(response, messageId, delta);
|
|
399
|
-
},
|
|
400
|
-
onTextDone: (doneText) => {
|
|
401
|
-
if (text.length === 0) {
|
|
402
|
-
text = doneText;
|
|
403
|
-
writeResponsesTextDelta(response, messageId, doneText);
|
|
404
|
-
}
|
|
405
|
-
},
|
|
406
|
-
onRuntimeEvent: (event) => {
|
|
407
|
-
writeResponsesRuntimeEvent(response, responseId, event);
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
clearInterval(heartbeat);
|
|
411
|
-
const outputText = text || getChatCompletionText(payload);
|
|
412
|
-
if (text.length === 0 && outputText.length > 0) {
|
|
413
|
-
writeResponsesTextDelta(response, messageId, outputText);
|
|
414
|
-
}
|
|
415
|
-
writeResponsesStreamDone(response, responseId, normalized.model, messageId, outputText);
|
|
416
|
-
response.end();
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const payload = await context.executeChatCompletion(chatPayload, {
|
|
420
|
-
timeoutMs: normalized.timeoutMs,
|
|
421
|
-
});
|
|
422
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
423
|
-
response.end(JSON.stringify(buildResponsesPayload(payload)));
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (method !== "POST") {
|
|
427
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
428
|
-
response.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
432
|
-
response.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
433
|
-
}
|
|
434
|
-
catch (error) {
|
|
435
|
-
const statusCode = mapBridgeErrorStatus(error);
|
|
436
|
-
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
437
|
-
response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error), code: isCliError(error) ? error.code : "BRIDGE_RUNTIME_FAILURE" } }));
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Converts one minimal Responses API payload into the existing chat-completions bridge call shape.
|
|
443
|
-
*/
|
|
444
|
-
function normalizeResponsesRequest(body) {
|
|
445
|
-
const payload = body;
|
|
446
|
-
if (typeof payload.model !== "string" || payload.model.trim() === "") {
|
|
447
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses requires a non-empty string model.");
|
|
448
|
-
}
|
|
449
|
-
const messages = normalizeResponsesInput(payload.input);
|
|
450
|
-
if (messages.length === 0) {
|
|
451
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses requires at least one input message.");
|
|
452
|
-
}
|
|
453
|
-
return {
|
|
454
|
-
model: payload.model,
|
|
455
|
-
messages,
|
|
456
|
-
stream: payload.stream === true,
|
|
457
|
-
timeoutMs: parseBridgeRequestTimeoutMs(body, "/v1/responses"),
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Extracts one optional request timeout for bridge-backed completions.
|
|
462
|
-
*/
|
|
463
|
-
function parseBridgeRequestTimeoutMs(body, endpoint) {
|
|
464
|
-
const timeoutMsValue = body.timeout_ms ?? body.timeoutMs;
|
|
465
|
-
if (timeoutMsValue === undefined) {
|
|
466
|
-
return undefined;
|
|
467
|
-
}
|
|
468
|
-
if (typeof timeoutMsValue !== "number" || !Number.isFinite(timeoutMsValue) || timeoutMsValue <= 0) {
|
|
469
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge ${endpoint} timeout must be a positive number when provided.`);
|
|
470
|
-
}
|
|
471
|
-
return timeoutMsValue;
|
|
472
|
-
}
|
|
473
|
-
function normalizeResponsesInput(input) {
|
|
474
|
-
if (typeof input === "string") {
|
|
475
|
-
return [{ role: "user", content: input }];
|
|
476
|
-
}
|
|
477
|
-
if (!Array.isArray(input)) {
|
|
478
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses expects input as a string or message array.");
|
|
479
|
-
}
|
|
480
|
-
if (input.length === 0) {
|
|
481
|
-
return [];
|
|
482
|
-
}
|
|
483
|
-
const entryKinds = input.map(classifyResponsesInputEntry);
|
|
484
|
-
const hasMessages = entryKinds.includes("message");
|
|
485
|
-
const hasContentItems = entryKinds.includes("content-item");
|
|
486
|
-
if (hasMessages && hasContentItems) {
|
|
487
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input array must contain either message objects or content items, not both.");
|
|
488
|
-
}
|
|
489
|
-
if (hasContentItems) {
|
|
490
|
-
return [
|
|
491
|
-
{
|
|
492
|
-
role: "user",
|
|
493
|
-
content: extractResponsesTextContent(input, 0),
|
|
494
|
-
},
|
|
495
|
-
];
|
|
496
|
-
}
|
|
497
|
-
return input.map((entry, index) => normalizeResponsesMessage(entry, index));
|
|
498
|
-
}
|
|
499
|
-
function normalizeResponsesMessage(entry, index) {
|
|
500
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
501
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}] must be an object message.`);
|
|
502
|
-
}
|
|
503
|
-
const message = entry;
|
|
504
|
-
const role = typeof message.role === "string" && message.role.trim() !== "" ? message.role : "user";
|
|
505
|
-
const content = extractResponsesTextContent(message.content, index);
|
|
506
|
-
return { role, content };
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Classifies one top-level Responses input entry so mixed array shapes can be rejected clearly.
|
|
510
|
-
*/
|
|
511
|
-
function classifyResponsesInputEntry(entry) {
|
|
512
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
513
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input entries must be objects.");
|
|
514
|
-
}
|
|
515
|
-
const record = entry;
|
|
516
|
-
if ("content" in record || "role" in record) {
|
|
517
|
-
return "message";
|
|
518
|
-
}
|
|
519
|
-
if (typeof record.type === "string") {
|
|
520
|
-
return "content-item";
|
|
521
|
-
}
|
|
522
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input entries must be message objects or typed content items.");
|
|
523
|
-
}
|
|
524
|
-
function extractResponsesTextContent(content, index) {
|
|
525
|
-
if (typeof content === "string") {
|
|
526
|
-
return content;
|
|
527
|
-
}
|
|
528
|
-
if (!Array.isArray(content)) {
|
|
529
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}].content must be a string or content item array.`);
|
|
530
|
-
}
|
|
531
|
-
const parts = content.map((item, itemIndex) => {
|
|
532
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
533
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}].content[${String(itemIndex)}] must be an object item.`);
|
|
534
|
-
}
|
|
535
|
-
return renderResponsesContentItem(item);
|
|
536
|
-
});
|
|
537
|
-
return parts.join("\n");
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Converts one Responses content item into the text-only prompt representation required by the Copilot SDK bridge.
|
|
541
|
-
*/
|
|
542
|
-
function renderResponsesContentItem(item) {
|
|
543
|
-
const type = typeof item.type === "string" ? item.type : null;
|
|
544
|
-
const text = typeof item.text === "string" ? item.text : null;
|
|
545
|
-
if ((type === "input_text" || type === "text" || type === "output_text") && text !== null) {
|
|
546
|
-
return text;
|
|
547
|
-
}
|
|
548
|
-
if (type === "input_image") {
|
|
549
|
-
return buildResponsesPlaceholder("input_image", item.image_url, item.file_id);
|
|
550
|
-
}
|
|
551
|
-
if (type === "input_file") {
|
|
552
|
-
return buildResponsesPlaceholder("input_file", item.filename, item.file_id);
|
|
553
|
-
}
|
|
554
|
-
if (type !== null) {
|
|
555
|
-
return `[unsupported content type: ${type}]`;
|
|
556
|
-
}
|
|
557
|
-
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses content items must declare a string type.");
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Builds a readable placeholder for non-text Responses content items preserved as text-only context.
|
|
561
|
-
*/
|
|
562
|
-
function buildResponsesPlaceholder(type, ...candidates) {
|
|
563
|
-
for (const candidate of candidates) {
|
|
564
|
-
if (typeof candidate === "string" && candidate.trim() !== "") {
|
|
565
|
-
return `[${type}: ${candidate}]`;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return `[${type} omitted]`;
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Converts the existing chat-completions response into a minimal Responses API payload.
|
|
572
|
-
*/
|
|
573
|
-
function buildResponsesPayload(payload) {
|
|
574
|
-
const firstChoice = Array.isArray(payload.choices) ? payload.choices[0] : null;
|
|
575
|
-
const outputText = firstChoice?.message?.content ?? "";
|
|
576
|
-
return {
|
|
577
|
-
id: payload.id ?? `resp_${Date.now()}`,
|
|
578
|
-
object: "response",
|
|
579
|
-
created_at: payload.created ?? Math.floor(Date.now() / 1000),
|
|
580
|
-
model: payload.model ?? "copilot",
|
|
581
|
-
status: "completed",
|
|
582
|
-
output: [
|
|
583
|
-
{
|
|
584
|
-
type: "message",
|
|
585
|
-
id: `${payload.id ?? "resp"}_msg_0`,
|
|
586
|
-
role: "assistant",
|
|
587
|
-
content: [
|
|
588
|
-
{
|
|
589
|
-
type: "output_text",
|
|
590
|
-
text: outputText,
|
|
591
|
-
},
|
|
592
|
-
],
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
output_text: outputText,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Emits a minimal OpenAI-compatible Responses API event stream.
|
|
600
|
-
*/
|
|
601
|
-
function writeResponsesStream(response, payload) {
|
|
602
|
-
const responsePayload = buildResponsesPayload(payload);
|
|
603
|
-
const responseId = typeof responsePayload.id === "string" ? responsePayload.id : `resp_${Date.now()}`;
|
|
604
|
-
const messageId = buildResponsesMessageId(responseId);
|
|
605
|
-
const outputText = typeof responsePayload.output_text === "string" ? responsePayload.output_text : "";
|
|
606
|
-
const inProgressResponse = {
|
|
607
|
-
...responsePayload,
|
|
608
|
-
status: "in_progress",
|
|
609
|
-
output: [],
|
|
610
|
-
};
|
|
611
|
-
const completedMessage = {
|
|
612
|
-
id: messageId,
|
|
613
|
-
type: "message",
|
|
614
|
-
status: "completed",
|
|
615
|
-
role: "assistant",
|
|
616
|
-
content: [
|
|
617
|
-
{
|
|
618
|
-
type: "output_text",
|
|
619
|
-
text: outputText,
|
|
620
|
-
annotations: [],
|
|
621
|
-
},
|
|
622
|
-
],
|
|
623
|
-
};
|
|
624
|
-
writeSseEvent(response, "response.created", {
|
|
625
|
-
type: "response.created",
|
|
626
|
-
response: inProgressResponse,
|
|
627
|
-
});
|
|
628
|
-
writeSseEvent(response, "response.in_progress", {
|
|
629
|
-
type: "response.in_progress",
|
|
630
|
-
response: inProgressResponse,
|
|
631
|
-
});
|
|
632
|
-
writeSseEvent(response, "response.output_item.added", {
|
|
633
|
-
type: "response.output_item.added",
|
|
634
|
-
output_index: 0,
|
|
635
|
-
item: {
|
|
636
|
-
id: messageId,
|
|
637
|
-
type: "message",
|
|
638
|
-
status: "in_progress",
|
|
639
|
-
role: "assistant",
|
|
640
|
-
content: [],
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
writeSseEvent(response, "response.content_part.added", {
|
|
644
|
-
type: "response.content_part.added",
|
|
645
|
-
item_id: messageId,
|
|
646
|
-
output_index: 0,
|
|
647
|
-
content_index: 0,
|
|
648
|
-
part: {
|
|
649
|
-
type: "output_text",
|
|
650
|
-
text: "",
|
|
651
|
-
annotations: [],
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
writeSseEvent(response, "response.output_text.delta", {
|
|
655
|
-
type: "response.output_text.delta",
|
|
656
|
-
item_id: messageId,
|
|
657
|
-
output_index: 0,
|
|
658
|
-
content_index: 0,
|
|
659
|
-
delta: outputText,
|
|
660
|
-
});
|
|
661
|
-
writeSseEvent(response, "response.output_text.done", {
|
|
662
|
-
type: "response.output_text.done",
|
|
663
|
-
item_id: messageId,
|
|
664
|
-
output_index: 0,
|
|
665
|
-
content_index: 0,
|
|
666
|
-
text: outputText,
|
|
667
|
-
});
|
|
668
|
-
writeSseEvent(response, "response.content_part.done", {
|
|
669
|
-
type: "response.content_part.done",
|
|
670
|
-
item_id: messageId,
|
|
671
|
-
output_index: 0,
|
|
672
|
-
content_index: 0,
|
|
673
|
-
part: {
|
|
674
|
-
type: "output_text",
|
|
675
|
-
text: outputText,
|
|
676
|
-
annotations: [],
|
|
677
|
-
},
|
|
678
|
-
});
|
|
679
|
-
writeSseEvent(response, "response.output_item.done", {
|
|
680
|
-
type: "response.output_item.done",
|
|
681
|
-
output_index: 0,
|
|
682
|
-
item: completedMessage,
|
|
683
|
-
});
|
|
684
|
-
writeSseEvent(response, "response.completed", {
|
|
685
|
-
type: "response.completed",
|
|
686
|
-
response: {
|
|
687
|
-
...responsePayload,
|
|
688
|
-
output: [completedMessage],
|
|
689
|
-
},
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
function writeResponsesStreamStart(response, responseId, model, messageId) {
|
|
693
|
-
const createdAt = Math.floor(Date.now() / 1000);
|
|
694
|
-
const inProgressResponse = {
|
|
695
|
-
id: responseId,
|
|
696
|
-
object: "response",
|
|
697
|
-
created_at: createdAt,
|
|
698
|
-
model,
|
|
699
|
-
status: "in_progress",
|
|
700
|
-
output: [],
|
|
701
|
-
output_text: "",
|
|
702
|
-
};
|
|
703
|
-
writeSseEvent(response, "response.created", {
|
|
704
|
-
type: "response.created",
|
|
705
|
-
response: inProgressResponse,
|
|
706
|
-
});
|
|
707
|
-
writeSseEvent(response, "response.in_progress", {
|
|
708
|
-
type: "response.in_progress",
|
|
709
|
-
response: inProgressResponse,
|
|
710
|
-
});
|
|
711
|
-
writeSseEvent(response, "response.output_item.added", {
|
|
712
|
-
type: "response.output_item.added",
|
|
713
|
-
output_index: 0,
|
|
714
|
-
item: {
|
|
715
|
-
id: messageId,
|
|
716
|
-
type: "message",
|
|
717
|
-
status: "in_progress",
|
|
718
|
-
role: "assistant",
|
|
719
|
-
content: [],
|
|
720
|
-
},
|
|
721
|
-
});
|
|
722
|
-
writeSseEvent(response, "response.content_part.added", {
|
|
723
|
-
type: "response.content_part.added",
|
|
724
|
-
item_id: messageId,
|
|
725
|
-
output_index: 0,
|
|
726
|
-
content_index: 0,
|
|
727
|
-
part: {
|
|
728
|
-
type: "output_text",
|
|
729
|
-
text: "",
|
|
730
|
-
annotations: [],
|
|
731
|
-
},
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
function writeResponsesTextDelta(response, messageId, delta) {
|
|
735
|
-
if (delta.length === 0) {
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
writeSseEvent(response, "response.output_text.delta", {
|
|
739
|
-
type: "response.output_text.delta",
|
|
740
|
-
item_id: messageId,
|
|
741
|
-
output_index: 0,
|
|
742
|
-
content_index: 0,
|
|
743
|
-
delta,
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
function writeResponsesStreamDone(response, responseId, model, messageId, outputText) {
|
|
747
|
-
const completedMessage = {
|
|
748
|
-
id: messageId,
|
|
749
|
-
type: "message",
|
|
750
|
-
status: "completed",
|
|
751
|
-
role: "assistant",
|
|
752
|
-
content: [
|
|
753
|
-
{
|
|
754
|
-
type: "output_text",
|
|
755
|
-
text: outputText,
|
|
756
|
-
annotations: [],
|
|
757
|
-
},
|
|
758
|
-
],
|
|
759
|
-
};
|
|
760
|
-
writeSseEvent(response, "response.output_text.done", {
|
|
761
|
-
type: "response.output_text.done",
|
|
762
|
-
item_id: messageId,
|
|
763
|
-
output_index: 0,
|
|
764
|
-
content_index: 0,
|
|
765
|
-
text: outputText,
|
|
766
|
-
});
|
|
767
|
-
writeSseEvent(response, "response.content_part.done", {
|
|
768
|
-
type: "response.content_part.done",
|
|
769
|
-
item_id: messageId,
|
|
770
|
-
output_index: 0,
|
|
771
|
-
content_index: 0,
|
|
772
|
-
part: {
|
|
773
|
-
type: "output_text",
|
|
774
|
-
text: outputText,
|
|
775
|
-
annotations: [],
|
|
776
|
-
},
|
|
777
|
-
});
|
|
778
|
-
writeSseEvent(response, "response.output_item.done", {
|
|
779
|
-
type: "response.output_item.done",
|
|
780
|
-
output_index: 0,
|
|
781
|
-
item: completedMessage,
|
|
782
|
-
});
|
|
783
|
-
writeSseEvent(response, "response.completed", {
|
|
784
|
-
type: "response.completed",
|
|
785
|
-
response: {
|
|
786
|
-
id: responseId,
|
|
787
|
-
object: "response",
|
|
788
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
789
|
-
model,
|
|
790
|
-
status: "completed",
|
|
791
|
-
output: [completedMessage],
|
|
792
|
-
output_text: outputText,
|
|
793
|
-
},
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
function writeResponsesRuntimeEvent(response, responseId, event) {
|
|
797
|
-
if (event.type === "assistant.message_delta") {
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
if (event.type === "assistant.reasoning_delta") {
|
|
801
|
-
writeResponsesReasoningDelta(response, responseId, formatRuntimeEventText(event));
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
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`);
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
const text = formatRuntimeEventText(event);
|
|
809
|
-
if (text.length === 0) {
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
writeResponsesCommentaryItem(response, responseId, text);
|
|
813
|
-
process.stderr.write(`[${new Date().toISOString()}] bridge runtime event type=${event.type} summary=${truncateBridgeText(text, 240)}\n`);
|
|
814
|
-
}
|
|
815
|
-
function writeResponsesReasoningDelta(response, responseId, text) {
|
|
816
|
-
const reasoningId = `${responseId}_rs_0`;
|
|
817
|
-
writeSseEvent(response, "response.reasoning_summary_part.added", {
|
|
818
|
-
type: "response.reasoning_summary_part.added",
|
|
819
|
-
item_id: reasoningId,
|
|
820
|
-
output_index: 0,
|
|
821
|
-
summary_index: 0,
|
|
822
|
-
part: {
|
|
823
|
-
type: "summary_text",
|
|
824
|
-
text: "",
|
|
825
|
-
},
|
|
826
|
-
});
|
|
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
|
-
}
|
|
835
|
-
function writeResponsesCommentaryItem(response, responseId, text) {
|
|
836
|
-
const itemId = `${responseId}_commentary_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
|
837
|
-
writeSseEvent(response, "response.output_item.done", {
|
|
838
|
-
type: "response.output_item.done",
|
|
839
|
-
output_index: 0,
|
|
840
|
-
item: {
|
|
841
|
-
id: itemId,
|
|
842
|
-
type: "message",
|
|
843
|
-
status: "completed",
|
|
844
|
-
role: "assistant",
|
|
845
|
-
phase: "commentary",
|
|
846
|
-
content: [
|
|
847
|
-
{
|
|
848
|
-
type: "output_text",
|
|
849
|
-
text,
|
|
850
|
-
annotations: [],
|
|
851
|
-
},
|
|
852
|
-
],
|
|
853
|
-
},
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
function formatRuntimeEventText(event) {
|
|
857
|
-
switch (event.type) {
|
|
858
|
-
case "assistant.intent":
|
|
859
|
-
return truncateBridgeText(event.text, 600);
|
|
860
|
-
case "assistant.reasoning_delta":
|
|
861
|
-
return truncateBridgeText(event.text, 600);
|
|
862
|
-
case "tool.execution_start":
|
|
863
|
-
return truncateBridgeText(`Copilot started ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
864
|
-
case "tool.execution_progress":
|
|
865
|
-
return truncateBridgeText(`Copilot progress ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
866
|
-
case "tool.execution_partial_result":
|
|
867
|
-
return truncateBridgeText(`Copilot partial result ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
868
|
-
case "tool.execution_complete":
|
|
869
|
-
return truncateBridgeText(`Copilot ${event.success === false ? "failed" : "completed"} ${formatToolName(event.name)}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
870
|
-
case "permission.requested":
|
|
871
|
-
return truncateBridgeText(`Copilot requested permission${event.kind ? `: ${event.kind}` : ""}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
872
|
-
case "permission.completed":
|
|
873
|
-
return truncateBridgeText(`Copilot permission ${event.approved === false ? "denied" : "approved"}${event.kind ? `: ${event.kind}` : ""}${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
874
|
-
case "user_input.requested":
|
|
875
|
-
return truncateBridgeText(`Copilot requested user input${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
876
|
-
case "exit_plan_mode.requested":
|
|
877
|
-
return truncateBridgeText(`Copilot requested to exit plan mode${formatRequestId(event.requestId)}${formatSummarySuffix(event.summary)}`, 600);
|
|
878
|
-
case "session.error":
|
|
879
|
-
return truncateBridgeText(`Copilot session error${formatSummarySuffix(event.summary)}`, 600);
|
|
880
|
-
case "session.idle":
|
|
881
|
-
return truncateBridgeText(event.summary, 600);
|
|
882
|
-
case "assistant.message_delta":
|
|
883
|
-
case "session.unknown":
|
|
884
|
-
return "";
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
function formatToolName(name) {
|
|
888
|
-
return name ? `tool ${name}` : "tool";
|
|
889
|
-
}
|
|
890
|
-
function formatRequestId(requestId) {
|
|
891
|
-
return requestId ? ` (${requestId})` : "";
|
|
892
|
-
}
|
|
893
|
-
function formatSummarySuffix(summary) {
|
|
894
|
-
return summary.length > 0 ? `: ${summary}` : "";
|
|
895
|
-
}
|
|
896
|
-
function truncateBridgeText(value, maxLength) {
|
|
897
|
-
if (value.length <= maxLength) {
|
|
898
|
-
return value;
|
|
899
|
-
}
|
|
900
|
-
return `${value.slice(0, maxLength)}... [truncated]`;
|
|
901
|
-
}
|
|
902
|
-
function startSseHeartbeat(response) {
|
|
903
|
-
return setInterval(() => {
|
|
904
|
-
response.write(": keep-alive\n\n");
|
|
905
|
-
}, 15000);
|
|
906
|
-
}
|
|
907
|
-
function getChatCompletionText(payload) {
|
|
908
|
-
return payload.choices?.[0]?.message?.content ?? "";
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* Formats and writes one server-sent event frame.
|
|
912
|
-
*/
|
|
913
|
-
function writeSseEvent(response, eventName, data) {
|
|
914
|
-
response.write(`event: ${eventName}\n`);
|
|
915
|
-
response.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* Derives a stable message identifier for synthesized Responses output items.
|
|
919
|
-
*/
|
|
920
|
-
function buildResponsesMessageId(responseId) {
|
|
921
|
-
if (responseId.startsWith("resp_")) {
|
|
922
|
-
return `msg_${responseId.slice("resp_".length)}_0`;
|
|
923
|
-
}
|
|
924
|
-
return `${responseId}_msg_0`;
|
|
925
|
-
}
|
|
926
|
-
function isCliError(error) {
|
|
927
|
-
return Boolean(error && typeof error === "object" && typeof error.code === "string");
|
|
928
|
-
}
|
|
929
|
-
function mapBridgeErrorStatus(error) {
|
|
930
|
-
if (!isCliError(error)) {
|
|
931
|
-
return 500;
|
|
932
|
-
}
|
|
933
|
-
if (error.code === "BRIDGE_UNSUPPORTED_REQUEST") {
|
|
934
|
-
return 400;
|
|
935
|
-
}
|
|
936
|
-
if (error.code === "COPILOT_AUTH_REQUIRED") {
|
|
937
|
-
return 401;
|
|
938
|
-
}
|
|
939
|
-
if (error.code === "BRIDGE_UPSTREAM_TIMEOUT") {
|
|
940
|
-
return 504;
|
|
941
|
-
}
|
|
942
|
-
return 500;
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* Returns a stable build identifier for the compiled bridge worker bundle.
|
|
946
|
-
*/
|
|
947
|
-
function getCopilotBridgeWorkerBuildId() {
|
|
948
|
-
if (cachedBridgeWorkerBuildId) {
|
|
949
|
-
return cachedBridgeWorkerBuildId;
|
|
950
|
-
}
|
|
951
|
-
const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
|
|
952
|
-
const stats = fs.statSync(workerPath);
|
|
953
|
-
cachedBridgeWorkerBuildId = `${stats.size}:${stats.mtimeMs}`;
|
|
954
|
-
return cachedBridgeWorkerBuildId;
|
|
955
|
-
}
|
|
956
|
-
/**
|
|
957
|
-
* Starts an in-process local bridge server. Primarily used by the worker entrypoint and tests.
|
|
958
|
-
*/
|
|
959
|
-
function startCopilotBridgeServer(args) {
|
|
960
|
-
return new Promise((resolve, reject) => {
|
|
961
|
-
const server = http.createServer(createCopilotBridgeRequestHandler({
|
|
962
|
-
apiKey: args.apiKey,
|
|
963
|
-
executeChatCompletion: args.executeChatCompletion,
|
|
964
|
-
}));
|
|
965
|
-
server.once("error", reject);
|
|
966
|
-
server.listen(args.port, args.host, () => {
|
|
967
|
-
server.off("error", reject);
|
|
968
|
-
resolve(server);
|
|
969
|
-
});
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* Polls the bridge health endpoint until it becomes available or the retry budget is exhausted.
|
|
974
|
-
*/
|
|
975
|
-
async function waitForCopilotBridgeHealth(host, port, attempts = 10, delayMs = 150) {
|
|
976
|
-
for (let index = 0; index < attempts; index += 1) {
|
|
977
|
-
const result = await probeBridgeEndpoint({ host, port, stage: "health" });
|
|
978
|
-
if (result.ok) {
|
|
979
|
-
return { ok: true };
|
|
980
|
-
}
|
|
981
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
982
|
-
}
|
|
983
|
-
return {
|
|
984
|
-
ok: false,
|
|
985
|
-
cause: "Timed out waiting for Copilot bridge health endpoint.",
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Stops the currently persisted Copilot bridge worker when possible.
|
|
990
|
-
*/
|
|
991
|
-
function stopCopilotBridge(runtimeDir) {
|
|
992
|
-
const state = (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir);
|
|
993
|
-
if (state?.pid) {
|
|
994
|
-
try {
|
|
995
|
-
process.kill(state.pid);
|
|
996
|
-
}
|
|
997
|
-
catch {
|
|
998
|
-
// Ignore best-effort bridge cleanup failures.
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
(0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
|
|
1002
|
-
}
|
|
1003
|
-
async function checkPortAvailability(host, port) {
|
|
1004
|
-
return new Promise((resolve) => {
|
|
1005
|
-
const server = net.createServer();
|
|
1006
|
-
server.once("error", (error) => {
|
|
1007
|
-
resolve({
|
|
1008
|
-
ok: false,
|
|
1009
|
-
cause: error.message,
|
|
1010
|
-
});
|
|
1011
|
-
});
|
|
1012
|
-
server.listen(port, host, () => {
|
|
1013
|
-
server.close((error) => {
|
|
1014
|
-
if (error) {
|
|
1015
|
-
resolve({
|
|
1016
|
-
ok: false,
|
|
1017
|
-
cause: error.message,
|
|
1018
|
-
});
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
resolve({ ok: true });
|
|
1022
|
-
});
|
|
1023
|
-
});
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
async function selectBridgePort(host, preferredPort) {
|
|
1027
|
-
const preferred = await checkPortAvailability(host, preferredPort);
|
|
1028
|
-
if (preferred.ok) {
|
|
1029
|
-
return preferredPort;
|
|
1030
|
-
}
|
|
1031
|
-
for (let port = 10000; port <= 99999; port += 1) {
|
|
1032
|
-
if (port === preferredPort) {
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
const available = await checkPortAvailability(host, port);
|
|
1036
|
-
if (available.ok) {
|
|
1037
|
-
return port;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
throw (0, errors_1.cliError)("BRIDGE_PORT_CONFLICT", "Unable to find a free 5-digit bridge port.", {
|
|
1041
|
-
host,
|
|
1042
|
-
port: preferredPort,
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs) {
|
|
1046
|
-
let startupFailure = null;
|
|
1047
|
-
const onError = (error) => {
|
|
1048
|
-
startupFailure = error.message;
|
|
1049
|
-
};
|
|
1050
|
-
const onExit = (code, signal) => {
|
|
1051
|
-
startupFailure = `Worker exited with code ${String(code)} signal ${String(signal)}.`;
|
|
1052
|
-
};
|
|
1053
|
-
child.once("error", onError);
|
|
1054
|
-
child.once("exit", onExit);
|
|
1055
|
-
try {
|
|
1056
|
-
for (let index = 0; index < attempts; index += 1) {
|
|
1057
|
-
if (startupFailure !== null) {
|
|
1058
|
-
return {
|
|
1059
|
-
ok: false,
|
|
1060
|
-
reason: "start-failed",
|
|
1061
|
-
cause: startupFailure,
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
const result = await probeBridgeEndpoint({ host, port, stage: "health" });
|
|
1065
|
-
if (result.ok) {
|
|
1066
|
-
return { ok: true };
|
|
1067
|
-
}
|
|
1068
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1069
|
-
}
|
|
1070
|
-
if (startupFailure !== null) {
|
|
1071
|
-
return {
|
|
1072
|
-
ok: false,
|
|
1073
|
-
reason: "start-failed",
|
|
1074
|
-
cause: startupFailure,
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
return {
|
|
1078
|
-
ok: false,
|
|
1079
|
-
reason: "healthcheck-failed",
|
|
1080
|
-
cause: "Timed out waiting for Copilot bridge health endpoint.",
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
finally {
|
|
1084
|
-
child.off("error", onError);
|
|
1085
|
-
child.off("exit", onExit);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
async function evaluateBridgeReuse(args) {
|
|
1089
|
-
const probeAt = new Date().toISOString();
|
|
1090
|
-
if (!args.current) {
|
|
1091
|
-
return {
|
|
1092
|
-
reuse: false,
|
|
1093
|
-
reason: "no persisted bridge state",
|
|
1094
|
-
replacedExisting: false,
|
|
1095
|
-
probeAt,
|
|
1096
|
-
logPath: args.logPath,
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
if (args.current.provider !== args.providerName) {
|
|
1100
|
-
return {
|
|
1101
|
-
reuse: false,
|
|
1102
|
-
reason: `provider mismatch: ${args.current.provider} -> ${args.providerName}`,
|
|
1103
|
-
replacedExisting: true,
|
|
1104
|
-
probeAt,
|
|
1105
|
-
logPath: args.logPath,
|
|
1106
|
-
probe: {
|
|
1107
|
-
ok: false,
|
|
1108
|
-
stage: "health",
|
|
1109
|
-
attempts: 0,
|
|
1110
|
-
cause: "provider-mismatch",
|
|
1111
|
-
retryable: false,
|
|
1112
|
-
message: "Persisted bridge provider does not match the requested provider.",
|
|
1113
|
-
},
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
if (args.current.baseUrl !== args.expectedBaseUrl) {
|
|
1117
|
-
return {
|
|
1118
|
-
reuse: false,
|
|
1119
|
-
reason: `base URL mismatch: ${args.current.baseUrl} -> ${args.expectedBaseUrl}`,
|
|
1120
|
-
replacedExisting: true,
|
|
1121
|
-
probeAt,
|
|
1122
|
-
logPath: args.logPath,
|
|
1123
|
-
probe: {
|
|
1124
|
-
ok: false,
|
|
1125
|
-
stage: "health",
|
|
1126
|
-
attempts: 0,
|
|
1127
|
-
cause: "base-url-mismatch",
|
|
1128
|
-
retryable: false,
|
|
1129
|
-
message: "Persisted bridge base URL does not match the requested provider runtime base URL.",
|
|
1130
|
-
},
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
if (args.current.workerBuildId !== args.workerBuildId) {
|
|
1134
|
-
return {
|
|
1135
|
-
reuse: false,
|
|
1136
|
-
reason: "worker build changed",
|
|
1137
|
-
replacedExisting: true,
|
|
1138
|
-
probeAt,
|
|
1139
|
-
logPath: args.logPath,
|
|
1140
|
-
probe: {
|
|
1141
|
-
ok: false,
|
|
1142
|
-
stage: "health",
|
|
1143
|
-
attempts: 0,
|
|
1144
|
-
cause: "worker-build-stale",
|
|
1145
|
-
retryable: false,
|
|
1146
|
-
message: "Persisted bridge worker build is stale.",
|
|
1147
|
-
},
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
const health = await probeBridgeEndpoint({
|
|
1151
|
-
host: args.current.host,
|
|
1152
|
-
port: args.current.port,
|
|
1153
|
-
stage: "health",
|
|
1154
|
-
});
|
|
1155
|
-
appendBridgeLifecycleLog(args.logPath, `probe stage=health attempts=${String(health.attempts)} ok=${String(health.ok)}${health.ok ? "" : ` retryable=${String(health.retryable)} cause=${health.cause}`}`);
|
|
1156
|
-
if (!health.ok) {
|
|
1157
|
-
return {
|
|
1158
|
-
reuse: false,
|
|
1159
|
-
reason: `health probe failed: ${health.message}`,
|
|
1160
|
-
replacedExisting: true,
|
|
1161
|
-
probeAt,
|
|
1162
|
-
logPath: args.logPath,
|
|
1163
|
-
probe: health,
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
const auth = await probeBridgeEndpoint({
|
|
1167
|
-
host: args.current.host,
|
|
1168
|
-
port: args.current.port,
|
|
1169
|
-
stage: "auth",
|
|
1170
|
-
apiKey: args.expectedApiKey,
|
|
1171
|
-
});
|
|
1172
|
-
appendBridgeLifecycleLog(args.logPath, `probe stage=auth attempts=${String(auth.attempts)} ok=${String(auth.ok)}${auth.ok ? "" : ` retryable=${String(auth.retryable)} cause=${auth.cause}`}`);
|
|
1173
|
-
if (!auth.ok) {
|
|
1174
|
-
return {
|
|
1175
|
-
reuse: false,
|
|
1176
|
-
reason: `auth probe failed: ${auth.message}`,
|
|
1177
|
-
replacedExisting: true,
|
|
1178
|
-
probeAt,
|
|
1179
|
-
logPath: args.logPath,
|
|
1180
|
-
probe: auth,
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
return {
|
|
1184
|
-
reuse: true,
|
|
1185
|
-
health,
|
|
1186
|
-
auth,
|
|
1187
|
-
probeAt,
|
|
1188
|
-
logPath: args.logPath,
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
async function probeBridgeEndpoint(args) {
|
|
1192
|
-
for (let attempt = 1; attempt <= BRIDGE_REUSE_ATTEMPTS; attempt += 1) {
|
|
1193
|
-
const result = await requestBridgeProbe(args);
|
|
1194
|
-
if (result.ok) {
|
|
1195
|
-
return {
|
|
1196
|
-
ok: true,
|
|
1197
|
-
stage: args.stage,
|
|
1198
|
-
attempts: attempt,
|
|
1199
|
-
};
|
|
1200
|
-
}
|
|
1201
|
-
if (!result.retryable || attempt === BRIDGE_REUSE_ATTEMPTS) {
|
|
1202
|
-
return {
|
|
1203
|
-
...result,
|
|
1204
|
-
stage: args.stage,
|
|
1205
|
-
attempts: attempt,
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
await new Promise((resolve) => setTimeout(resolve, BRIDGE_REUSE_DELAY_MS));
|
|
1209
|
-
}
|
|
1210
|
-
return {
|
|
1211
|
-
ok: false,
|
|
1212
|
-
stage: args.stage,
|
|
1213
|
-
attempts: BRIDGE_REUSE_ATTEMPTS,
|
|
1214
|
-
cause: "transport-timeout",
|
|
1215
|
-
retryable: true,
|
|
1216
|
-
message: "Bridge probe timed out.",
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
async function requestBridgeProbe(args) {
|
|
1220
|
-
return new Promise((resolve) => {
|
|
1221
|
-
const request = http.request({
|
|
1222
|
-
host: args.host,
|
|
1223
|
-
port: args.port,
|
|
1224
|
-
method: "GET",
|
|
1225
|
-
path: args.stage === "health" ? "/healthz" : "/v1/models",
|
|
1226
|
-
timeout: BRIDGE_REUSE_TIMEOUT_MS,
|
|
1227
|
-
headers: args.stage === "auth"
|
|
1228
|
-
? {
|
|
1229
|
-
authorization: `Bearer ${args.apiKey ?? ""}`,
|
|
1230
|
-
}
|
|
1231
|
-
: undefined,
|
|
1232
|
-
}, (response) => {
|
|
1233
|
-
response.resume();
|
|
1234
|
-
if (response.statusCode === 200) {
|
|
1235
|
-
resolve({ ok: true });
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
if (args.stage === "auth" && (response.statusCode === 401 || response.statusCode === 403)) {
|
|
1239
|
-
resolve({
|
|
1240
|
-
ok: false,
|
|
1241
|
-
cause: "auth-rejected",
|
|
1242
|
-
retryable: false,
|
|
1243
|
-
statusCode: response.statusCode,
|
|
1244
|
-
message: `Authorization probe returned status ${String(response.statusCode)}.`,
|
|
1245
|
-
});
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
resolve({
|
|
1249
|
-
ok: false,
|
|
1250
|
-
cause: args.stage === "health" ? "health-non-200" : "transport-error",
|
|
1251
|
-
retryable: false,
|
|
1252
|
-
statusCode: response.statusCode,
|
|
1253
|
-
message: args.stage === "health"
|
|
1254
|
-
? `Health endpoint returned status ${String(response.statusCode ?? 0)}.`
|
|
1255
|
-
: `Authorization probe returned status ${String(response.statusCode ?? 0)}.`,
|
|
1256
|
-
});
|
|
1257
|
-
});
|
|
1258
|
-
request.on("error", (error) => {
|
|
1259
|
-
const classified = classifyProbeTransportError(error);
|
|
1260
|
-
resolve({
|
|
1261
|
-
ok: false,
|
|
1262
|
-
cause: classified.cause,
|
|
1263
|
-
retryable: classified.retryable,
|
|
1264
|
-
message: error.message,
|
|
1265
|
-
});
|
|
1266
|
-
});
|
|
1267
|
-
request.on("timeout", () => {
|
|
1268
|
-
request.destroy(new Error(args.stage === "health" ? "Health endpoint timed out." : "Authorization probe timed out."));
|
|
1269
|
-
});
|
|
1270
|
-
request.end();
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
function classifyProbeTransportError(error) {
|
|
1274
|
-
const message = error.message.toLowerCase();
|
|
1275
|
-
if (message.includes("timed out") ||
|
|
1276
|
-
message.includes("econnrefused") ||
|
|
1277
|
-
message.includes("econnreset") ||
|
|
1278
|
-
message.includes("socket hang up") ||
|
|
1279
|
-
message.includes("epipe")) {
|
|
1280
|
-
return {
|
|
1281
|
-
cause: message.includes("timed out") ? "transport-timeout" : "transport-error",
|
|
1282
|
-
retryable: true,
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
return {
|
|
1286
|
-
cause: "transport-error",
|
|
1287
|
-
retryable: false,
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
function ensureBridgeLogFile(logPath) {
|
|
1291
|
-
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
1292
|
-
if (!fs.existsSync(logPath)) {
|
|
1293
|
-
fs.writeFileSync(logPath, "", "utf8");
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
function openBridgeLogFd(logPath) {
|
|
1297
|
-
ensureBridgeLogFile(logPath);
|
|
1298
|
-
return fs.openSync(logPath, "a");
|
|
1299
|
-
}
|
|
1300
|
-
function appendBridgeLifecycleLog(logPath, message) {
|
|
1301
|
-
ensureBridgeLogFile(logPath);
|
|
1302
|
-
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`, "utf8");
|
|
1303
|
-
}
|
|
1304
|
-
async function readJsonBody(request) {
|
|
1305
|
-
const chunks = [];
|
|
1306
|
-
for await (const chunk of request) {
|
|
1307
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
1308
|
-
}
|
|
1309
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1310
|
-
return raw.trim() === "" ? {} : JSON.parse(raw);
|
|
1311
|
-
}
|
|
1312
|
-
function isAuthorized(request, expectedApiKey) {
|
|
1313
|
-
const authorization = request.headers.authorization;
|
|
1314
|
-
if (!authorization || !authorization.startsWith("Bearer ")) {
|
|
1315
|
-
return false;
|
|
1316
|
-
}
|
|
1317
|
-
return authorization.slice("Bearer ".length) === expectedApiKey;
|
|
1318
|
-
}
|