codex-copilot-dx 0.1.3 → 0.1.5
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 +3 -1
- package/package.json +1 -1
- package/src/adapter.mjs +72 -18
package/README.md
CHANGED
|
@@ -20,9 +20,11 @@ Supports both HTTP SSE streaming and WebSocket connections.
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npx codex-copilot-dx
|
|
23
|
+
npx codex-copilot-dx@latest
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
> Tip: the `@latest` suffix forces `npx` to fetch the newest release instead of using a stale cached copy.
|
|
27
|
+
|
|
26
28
|
On first run, it will:
|
|
27
29
|
1. Authenticate with GitHub via device flow (if needed)
|
|
28
30
|
2. Start `copilot-api` proxy (port 4141)
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -100,18 +100,42 @@ async function proxyCopilotResponsesWS(reqBody, wsSend, onDone) {
|
|
|
100
100
|
const reader = resp.body.getReader();
|
|
101
101
|
const decoder = new TextDecoder();
|
|
102
102
|
let buf = "";
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
let currentEvent = null;
|
|
104
|
+
let sawCompleted = false;
|
|
105
|
+
let lastResponseId = `resp_${uid()}`;
|
|
106
|
+
let lastModel = reqBody.model || "unknown";
|
|
107
|
+
try {
|
|
108
|
+
while (true) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done) break;
|
|
111
|
+
buf += decoder.decode(value, { stream: true });
|
|
112
|
+
const lines = buf.split("\n");
|
|
113
|
+
buf = lines.pop();
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (line.startsWith("event: ")) { currentEvent = line.slice(7).trim(); continue; }
|
|
116
|
+
if (!line.startsWith("data: ")) { if (line === "") currentEvent = null; continue; }
|
|
117
|
+
const data = line.slice(6).trim();
|
|
118
|
+
if (data === "[DONE]") continue;
|
|
119
|
+
let parsed;
|
|
120
|
+
try { parsed = JSON.parse(data); } catch { continue; }
|
|
121
|
+
if (!parsed.type && currentEvent) parsed.type = currentEvent;
|
|
122
|
+
if (!parsed.type) continue;
|
|
123
|
+
if (parsed.response?.id) lastResponseId = parsed.response.id;
|
|
124
|
+
if (parsed.response?.model) lastModel = parsed.response.model;
|
|
125
|
+
if (parsed.type === "response.completed") sawCompleted = true;
|
|
126
|
+
wsSend(JSON.stringify(parsed));
|
|
127
|
+
}
|
|
114
128
|
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
wsSend(JSON.stringify({ type: "error", error: { message: e.message || String(e), code: 500 } }));
|
|
131
|
+
onDone();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!sawCompleted) {
|
|
135
|
+
wsSend(JSON.stringify({
|
|
136
|
+
type: "response.completed",
|
|
137
|
+
response: { id: lastResponseId, object: "response", status: "completed", model: lastModel, output: [], usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } },
|
|
138
|
+
}));
|
|
115
139
|
}
|
|
116
140
|
onDone();
|
|
117
141
|
}
|
|
@@ -179,7 +203,23 @@ function processStream(upstreamRes, emitEvent, onDone) {
|
|
|
179
203
|
emitEvent("response.output_item.added", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "in_progress", content: [] } });
|
|
180
204
|
emitEvent("response.content_part.added", { output_index: 0, content_index: 0, part: { type: "output_text", text: "" } });
|
|
181
205
|
|
|
182
|
-
let buf = "", fullText = "", toolCalls = {}, hasToolCalls = false;
|
|
206
|
+
let buf = "", fullText = "", toolCalls = {}, hasToolCalls = false, finished = false;
|
|
207
|
+
|
|
208
|
+
const finish = (errMsg) => {
|
|
209
|
+
if (finished) return;
|
|
210
|
+
finished = true;
|
|
211
|
+
if (errMsg) {
|
|
212
|
+
emitEvent("error", { error: { message: errMsg, code: 500 } });
|
|
213
|
+
} else {
|
|
214
|
+
emitEvent("response.output_text.done", { output_index: 0, content_index: 0, text: fullText });
|
|
215
|
+
emitEvent("response.output_item.done", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] } });
|
|
216
|
+
const output = hasToolCalls
|
|
217
|
+
? Object.entries(toolCalls).map(([id, tc]) => ({ type: "function_call", id, call_id: id, name: tc.name, arguments: tc.arguments, status: "completed" }))
|
|
218
|
+
: [{ type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] }];
|
|
219
|
+
emitEvent("response.completed", { response: { id: respId, object: "response", status: "completed", model: actualModel, output, usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } } });
|
|
220
|
+
}
|
|
221
|
+
onDone();
|
|
222
|
+
};
|
|
183
223
|
|
|
184
224
|
upstreamRes.on("data", (chunk) => {
|
|
185
225
|
buf += chunk.toString();
|
|
@@ -197,6 +237,7 @@ function processStream(upstreamRes, emitEvent, onDone) {
|
|
|
197
237
|
? Object.entries(toolCalls).map(([id, tc]) => ({ type: "function_call", id, call_id: id, name: tc.name, arguments: tc.arguments, status: "completed" }))
|
|
198
238
|
: [{ type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] }];
|
|
199
239
|
emitEvent("response.completed", { response: { id: respId, object: "response", status: "completed", model: actualModel, output, usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } } });
|
|
240
|
+
finished = true;
|
|
200
241
|
onDone();
|
|
201
242
|
return;
|
|
202
243
|
}
|
|
@@ -217,8 +258,8 @@ function processStream(upstreamRes, emitEvent, onDone) {
|
|
|
217
258
|
}
|
|
218
259
|
}
|
|
219
260
|
});
|
|
220
|
-
upstreamRes.on("end", () =>
|
|
221
|
-
upstreamRes.on("error", () =>
|
|
261
|
+
upstreamRes.on("end", () => finish());
|
|
262
|
+
upstreamRes.on("error", (e) => finish(e?.message || "upstream stream error"));
|
|
222
263
|
}
|
|
223
264
|
|
|
224
265
|
function forwardToChat(chatReq, emitEvent, onDone, onError) {
|
|
@@ -274,7 +315,16 @@ function wsSend(socket, data, opcode = 0x1) {
|
|
|
274
315
|
}
|
|
275
316
|
|
|
276
317
|
async function handleWebSocket(req, socket) {
|
|
277
|
-
|
|
318
|
+
let rawKey = req.headers["sec-websocket-key"];
|
|
319
|
+
if (Array.isArray(rawKey)) rawKey = rawKey[0];
|
|
320
|
+
const key = (rawKey || "").trim();
|
|
321
|
+
const upgrade = String(req.headers["upgrade"] || "").toLowerCase();
|
|
322
|
+
const connection = String(req.headers["connection"] || "").toLowerCase();
|
|
323
|
+
if (!key || upgrade !== "websocket" || !connection.includes("upgrade")) {
|
|
324
|
+
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
325
|
+
socket.destroy();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
278
328
|
const accept = createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-5AB9FC6AB199").digest("base64");
|
|
279
329
|
socket.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n");
|
|
280
330
|
|
|
@@ -357,9 +407,13 @@ export function startAdapter(port = 4142) {
|
|
|
357
407
|
});
|
|
358
408
|
|
|
359
409
|
server.on("upgrade", (req, socket) => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
410
|
+
// Codex Desktop 0.130+ negotiates a "responses_websockets" server-push
|
|
411
|
+
// protocol on WS upgrade. We don't implement that protocol; accepting the
|
|
412
|
+
// upgrade and waiting for a client request just hangs and triggers a
|
|
413
|
+
// 5-attempt reconnect storm. Refuse the upgrade so Codex falls back to
|
|
414
|
+
// plain HTTP SSE, which this adapter handles correctly.
|
|
415
|
+
socket.write("HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
416
|
+
socket.destroy();
|
|
363
417
|
});
|
|
364
418
|
|
|
365
419
|
return new Promise((resolve) => {
|