codex-copilot-dx 0.1.4 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/adapter.mjs +72 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Use Codex Desktop app with your GitHub Copilot subscription",
5
5
  "type": "module",
6
6
  "bin": {
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
- while (true) {
104
- const { done, value } = await reader.read();
105
- if (done) break;
106
- buf += decoder.decode(value, { stream: true });
107
- const lines = buf.split("\n");
108
- buf = lines.pop();
109
- for (const line of lines) {
110
- if (!line.startsWith("data: ")) continue;
111
- const data = line.slice(6).trim();
112
- if (data === "[DONE]") continue;
113
- try { wsSend(JSON.stringify(JSON.parse(data))); } catch {}
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", () => onDone());
221
- upstreamRes.on("error", () => onDone());
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
- const key = req.headers["sec-websocket-key"];
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
- if (req.url?.startsWith("/v1/responses")) {
361
- handleWebSocket(req, socket).catch((e) => { console.error("[codex-copilot-dx] WS error:", e.message); socket.destroy(); });
362
- } else socket.destroy();
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) => {