codex-copilot-dx 0.1.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/adapter.mjs CHANGED
@@ -1,13 +1,9 @@
1
1
  import http from "node:http";
2
- import https from "node:https";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import os from "node:os";
6
- import { randomUUID, createHash } from "node:crypto";
7
-
8
- const COPILOT_API_PORT = parseInt(process.env.COPILOT_API_PORT || "4141");
9
- const COPILOT_API = "https://api.githubcopilot.com";
10
- const GITHUB_API = "https://api.github.com";
2
+ import { randomUUID } from "node:crypto";
3
+ import { chatCompletions, listModels, responses as copilotResponses, responsesCompact as copilotResponsesCompact } from "./copilot.mjs";
4
+ import { webStreamLines } from "./stream.mjs";
5
+ import { anthropicToChat, chatToAnthropic, streamAnthropicFromLines, countTokens } from "./anthropic.mjs";
6
+ import { status } from "./status.mjs";
11
7
 
12
8
  // Models that only support Responses API (not chat/completions)
13
9
  const RESPONSES_ONLY = new Set([
@@ -18,129 +14,129 @@ const RESPONSES_ONLY = new Set([
18
14
  "gpt-5.2-codex",
19
15
  ]);
20
16
 
21
- // ── Copilot Token ──
17
+ // Direct Copilot Responses API proxy.
22
18
 
23
- let copilotToken = null;
24
- let copilotTokenExpiry = 0;
19
+ const RESPONSE_HISTORY_LIMIT = 64;
20
+ const responseHistories = new Map();
25
21
 
26
- function getGithubToken() {
27
- const p = path.join(os.homedir(), ".local", "share", "copilot-api", "github_token");
28
- if (!fs.existsSync(p)) throw new Error("GitHub token not found. Run: npx copilot-api auth");
29
- return fs.readFileSync(p, "utf-8").trim();
22
+ export function requestPath(reqUrl) {
23
+ return new URL(reqUrl || "/", "http://localhost").pathname;
30
24
  }
31
25
 
32
- async function getCopilotToken() {
33
- if (copilotToken && Date.now() < copilotTokenExpiry - 60000) return copilotToken;
34
- const ghToken = getGithubToken();
35
- const resp = await fetch(`${GITHUB_API}/copilot_internal/v2/token`, {
36
- headers: { Authorization: `token ${ghToken}`, Accept: "application/json" },
37
- });
38
- if (!resp.ok) throw new Error(`Failed to get Copilot token: ${resp.status}`);
39
- const data = await resp.json();
40
- copilotToken = data.token;
41
- copilotTokenExpiry = data.expires_at * 1000;
42
- console.log("[codex-copilot-dx] Copilot token refreshed");
43
- return copilotToken;
26
+ function cloneJson(value) {
27
+ return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
44
28
  }
45
29
 
46
- // ── Direct Copilot Responses API proxy ──
47
-
48
- async function proxyCopilotResponses(reqBody, req, res) {
49
- const token = await getCopilotToken();
50
- const resp = await fetch(`${COPILOT_API}/v1/responses`, {
51
- method: "POST",
52
- headers: {
53
- Authorization: `Bearer ${token}`,
54
- "Content-Type": "application/json",
55
- "Editor-Version": "vscode/1.90.0",
56
- "Copilot-Integration-Id": "vscode-chat",
57
- },
58
- body: JSON.stringify(reqBody),
59
- });
30
+ function responsesInputItems(input) {
31
+ if (input === undefined || input === null) return [];
32
+ if (typeof input === "string") {
33
+ return [{ type: "message", role: "user", content: [{ type: "input_text", text: input }] }];
34
+ }
35
+ return Array.isArray(input) ? cloneJson(input) : [cloneJson(input)];
36
+ }
37
+
38
+ function responsesOutputItems(output) {
39
+ if (!Array.isArray(output)) return [];
40
+ return cloneJson(output.filter((item) => item?.type === "message" || item?.type === "function_call"));
41
+ }
42
+
43
+ export function clearResponseHistoryForTests() {
44
+ responseHistories.clear();
45
+ }
60
46
 
61
- if (reqBody.stream) {
47
+ export function prepareResponsesRequest(reqBody) {
48
+ const body = cloneJson(reqBody);
49
+ const currentInputItems = responsesInputItems(body.input);
50
+ const previousId = body.previous_response_id;
51
+
52
+ if (previousId !== undefined && previousId !== null) {
53
+ const previousItems = responseHistories.get(previousId);
54
+ if (!previousItems) {
55
+ const err = new Error(`previous_response_id is not available in local adapter history: ${previousId}`);
56
+ err.statusCode = 400;
57
+ throw err;
58
+ }
59
+ body.input = [...cloneJson(previousItems), ...currentInputItems];
60
+ } else {
61
+ body.input = currentInputItems;
62
+ }
63
+
64
+ delete body.previous_response_id;
65
+ delete body.store;
66
+
67
+ return { body, inputItems: body.input };
68
+ }
69
+
70
+ export function rememberResponseHistory(reqContext, responseJson) {
71
+ if (!responseJson?.id || !Array.isArray(reqContext?.inputItems)) return;
72
+ const items = [...cloneJson(reqContext.inputItems), ...responsesOutputItems(responseJson.output)];
73
+ responseHistories.set(responseJson.id, items);
74
+ while (responseHistories.size > RESPONSE_HISTORY_LIMIT) {
75
+ responseHistories.delete(responseHistories.keys().next().value);
76
+ }
77
+ }
78
+
79
+ function storeCompletedResponseFromSse(reqContext, data) {
80
+ if (!data || data === "[DONE]") return;
81
+ try {
82
+ const event = JSON.parse(data);
83
+ const response = event.response;
84
+ if (response?.id && response.status === "completed") rememberResponseHistory(reqContext, response);
85
+ } catch {
86
+ // Ignore non-JSON stream fragments.
87
+ }
88
+ }
89
+
90
+ function readSseEvents(buffer, onData) {
91
+ while (true) {
92
+ const match = buffer.match(/\r?\n\r?\n/);
93
+ if (!match) return buffer;
94
+ const chunk = buffer.slice(0, match.index);
95
+ buffer = buffer.slice(match.index + match[0].length);
96
+ const data = chunk
97
+ .split(/\r?\n/)
98
+ .filter((line) => line.startsWith("data:"))
99
+ .map((line) => line.slice(5).trimStart())
100
+ .join("\n");
101
+ if (data) onData(data);
102
+ }
103
+ }
104
+
105
+ async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses) {
106
+ const resp = await upstream(reqContext.body);
107
+
108
+ if (reqContext.body.stream) {
62
109
  res.writeHead(resp.status, {
63
110
  "Content-Type": resp.headers.get("content-type") || "text/event-stream",
64
111
  "Cache-Control": "no-cache",
65
112
  Connection: "keep-alive",
66
113
  });
67
114
  const reader = resp.body.getReader();
115
+ const decoder = new TextDecoder();
116
+ let buffer = "";
68
117
  while (true) {
69
118
  const { done, value } = await reader.read();
70
- if (done) { res.end(); return; }
119
+ if (done) {
120
+ buffer += decoder.decode();
121
+ readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
122
+ res.end();
123
+ return;
124
+ }
125
+ buffer += decoder.decode(value, { stream: true });
126
+ buffer = readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
71
127
  res.write(value);
72
128
  }
73
129
  } else {
74
130
  const data = await resp.text();
75
131
  res.writeHead(resp.status, { "Content-Type": "application/json" });
76
132
  res.end(data);
77
- }
78
- }
79
-
80
- async function proxyCopilotResponsesWS(reqBody, wsSend, onDone) {
81
- const token = await getCopilotToken();
82
- const resp = await fetch(`${COPILOT_API}/v1/responses`, {
83
- method: "POST",
84
- headers: {
85
- Authorization: `Bearer ${token}`,
86
- "Content-Type": "application/json",
87
- "Editor-Version": "vscode/1.90.0",
88
- "Copilot-Integration-Id": "vscode-chat",
89
- },
90
- body: JSON.stringify({ ...reqBody, stream: true }),
91
- });
92
-
93
- if (!resp.ok) {
94
- const err = await resp.text();
95
- wsSend(JSON.stringify({ type: "error", error: { message: err, code: resp.status } }));
96
- onDone();
97
- return;
98
- }
99
-
100
- const reader = resp.body.getReader();
101
- const decoder = new TextDecoder();
102
- let buf = "";
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
- }
133
+ if (resp.ok) {
134
+ try { rememberResponseHistory(reqContext, JSON.parse(data)); } catch {}
128
135
  }
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
- }));
139
136
  }
140
- onDone();
141
137
  }
142
138
 
143
- // ── Chat Completions conversion (for older models via copilot-api) ──
139
+ // Chat Completions conversion for older models.
144
140
 
145
141
  function responsesToChat(body) {
146
142
  const messages = [];
@@ -195,52 +191,42 @@ function chatToResponses(chatResp, model) {
195
191
  return { id, object: "response", status: "completed", model: chatResp.model || model, output, usage: chatResp.usage ? { input_tokens: chatResp.usage.prompt_tokens || 0, output_tokens: chatResp.usage.completion_tokens || 0, total_tokens: chatResp.usage.total_tokens || 0 } : undefined };
196
192
  }
197
193
 
198
- function processStream(upstreamRes, emitEvent, onDone) {
194
+ async function forwardToChat(chatReq, emitEvent, onDone, onError) {
195
+ delete chatReq.max_tokens;
196
+ let resp;
197
+ try {
198
+ resp = await chatCompletions({ ...chatReq, stream: true });
199
+ } catch (e) {
200
+ onError(502, e.message);
201
+ return;
202
+ }
203
+ if (!resp.ok) {
204
+ onError(resp.status, await resp.text());
205
+ return;
206
+ }
199
207
  const respId = `resp_${uid()}`, itemId = `item_${uid()}`;
200
- let actualModel = "unknown";
208
+ let actualModel = "unknown", fullText = "", toolCalls = {}, hasToolCalls = false;
201
209
 
202
210
  emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
203
211
  emitEvent("response.output_item.added", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "in_progress", content: [] } });
204
212
  emitEvent("response.content_part.added", { output_index: 0, content_index: 0, part: { type: "output_text", text: "" } });
205
213
 
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
+ const emitCompleted = () => {
215
+ if (!hasToolCalls) {
214
216
  emitEvent("response.output_text.done", { output_index: 0, content_index: 0, text: fullText });
215
217
  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
218
  }
221
- onDone();
219
+ const output = hasToolCalls
220
+ ? Object.entries(toolCalls).map(([id, tc]) => ({ type: "function_call", id, call_id: id, name: tc.name, arguments: tc.arguments, status: "completed" }))
221
+ : [{ type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] }];
222
+ emitEvent("response.completed", { response: { id: respId, object: "response", status: "completed", model: actualModel, output, usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } } });
222
223
  };
223
224
 
224
- upstreamRes.on("data", (chunk) => {
225
- buf += chunk.toString();
226
- let lines = buf.split("\n");
227
- buf = lines.pop();
228
- for (const line of lines) {
225
+ try {
226
+ for await (const line of webStreamLines(resp)) {
229
227
  if (!line.startsWith("data: ")) continue;
230
228
  const data = line.slice(6).trim();
231
- if (data === "[DONE]") {
232
- if (!hasToolCalls) {
233
- emitEvent("response.output_text.done", { output_index: 0, content_index: 0, text: fullText });
234
- emitEvent("response.output_item.done", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] } });
235
- }
236
- const output = hasToolCalls
237
- ? Object.entries(toolCalls).map(([id, tc]) => ({ type: "function_call", id, call_id: id, name: tc.name, arguments: tc.arguments, status: "completed" }))
238
- : [{ type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] }];
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;
241
- onDone();
242
- return;
243
- }
229
+ if (data === "[DONE]") { emitCompleted(); onDone(); return; }
244
230
  let parsed;
245
231
  try { parsed = JSON.parse(data); } catch { continue; }
246
232
  if (parsed.model) actualModel = parsed.model;
@@ -257,153 +243,147 @@ function processStream(upstreamRes, emitEvent, onDone) {
257
243
  }
258
244
  }
259
245
  }
260
- });
261
- upstreamRes.on("end", () => finish());
262
- upstreamRes.on("error", (e) => finish(e?.message || "upstream stream error"));
263
- }
264
-
265
- function forwardToChat(chatReq, emitEvent, onDone, onError) {
266
- delete chatReq.max_tokens;
267
- const body = JSON.stringify(chatReq);
268
- const opts = { hostname: "localhost", port: COPILOT_API_PORT, path: "/v1/chat/completions", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } };
269
- const proxy = http.request(opts, (upRes) => {
270
- if (upRes.statusCode !== 200) { let d = ""; upRes.on("data", c => d += c); upRes.on("end", () => onError(upRes.statusCode, d)); return; }
271
- processStream(upRes, emitEvent, onDone);
272
- });
273
- proxy.on("error", (e) => onError(502, e.message));
274
- proxy.write(body);
275
- proxy.end();
276
- }
277
-
278
- // ── WebSocket (RFC 6455 minimal) ──
279
-
280
- function wsRead(socket) {
281
- return new Promise((resolve, reject) => {
282
- const chunks = [];
283
- const onData = (data) => {
284
- chunks.push(data);
285
- const buf = Buffer.concat(chunks);
286
- if (buf.length < 2) return;
287
- const masked = (buf[1] & 0x80) !== 0;
288
- let payloadLen = buf[1] & 0x7f, offset = 2;
289
- if (payloadLen === 126) { if (buf.length < 4) return; payloadLen = buf.readUInt16BE(2); offset = 4; }
290
- else if (payloadLen === 127) { if (buf.length < 10) return; payloadLen = Number(buf.readBigUInt64BE(2)); offset = 10; }
291
- const maskLen = masked ? 4 : 0;
292
- if (buf.length < offset + maskLen + payloadLen) return;
293
- let payload = buf.slice(offset + maskLen, offset + maskLen + payloadLen);
294
- if (masked) { const mask = buf.slice(offset, offset + 4); for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4]; }
295
- socket.removeListener("data", onData);
296
- const opcode = buf[0] & 0x0f;
297
- if (opcode === 0x8) resolve(null);
298
- else if (opcode === 0x9) { wsSend(socket, "", 0xa); chunks.length = 0; socket.on("data", onData); }
299
- else resolve(payload.toString("utf-8"));
300
- };
301
- socket.on("data", onData);
302
- socket.on("error", reject);
303
- socket.on("close", () => resolve(null));
304
- });
305
- }
306
-
307
- function wsSend(socket, data, opcode = 0x1) {
308
- const payload = typeof data === "string" ? Buffer.from(data) : data;
309
- const len = payload.length;
310
- let header;
311
- if (len < 126) { header = Buffer.alloc(2); header[0] = 0x80 | opcode; header[1] = len; }
312
- else if (len < 65536) { header = Buffer.alloc(4); header[0] = 0x80 | opcode; header[1] = 126; header.writeUInt16BE(len, 2); }
313
- else { header = Buffer.alloc(10); header[0] = 0x80 | opcode; header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2); }
314
- try { socket.write(Buffer.concat([header, payload])); } catch {}
315
- }
316
-
317
- async function handleWebSocket(req, socket) {
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();
246
+ } catch (e) {
247
+ onError(500, e?.message || "upstream stream error");
326
248
  return;
327
249
  }
328
- const accept = createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-5AB9FC6AB199").digest("base64");
329
- socket.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n");
330
-
331
- while (true) {
332
- const msg = await wsRead(socket);
333
- if (msg === null) break;
334
- let parsed;
335
- try { parsed = JSON.parse(msg); } catch { continue; }
336
- let reqBody = parsed;
337
- if (parsed.type === "response.create" && parsed.response) reqBody = parsed.response;
338
- const model = reqBody.model || "unknown";
339
-
340
- if (RESPONSES_ONLY.has(model)) {
341
- proxyCopilotResponsesWS(reqBody, (d) => wsSend(socket, d), () => {}).catch((e) => {
342
- wsSend(socket, JSON.stringify({ type: "error", error: { message: e.message, code: 500 } }));
343
- });
344
- } else {
345
- const chatReq = responsesToChat(reqBody);
346
- chatReq.stream = true;
347
- forwardToChat(chatReq, (event, data) => wsSend(socket, JSON.stringify({ type: event, ...data })), () => {}, (status, errMsg) => {
348
- wsSend(socket, JSON.stringify({ type: "error", error: { message: errMsg, code: status } }));
349
- });
350
- }
351
- }
352
- }
353
-
354
- // ── HTTP passthrough ──
355
-
356
- function proxyPassthrough(req, res) {
357
- const opts = { hostname: "localhost", port: COPILOT_API_PORT, path: req.url, method: req.method, headers: { ...req.headers, host: `localhost:${COPILOT_API_PORT}` } };
358
- const proxy = http.request(opts, (upRes) => { res.writeHead(upRes.statusCode, upRes.headers); upRes.pipe(res); });
359
- proxy.on("error", () => { if (!res.headersSent) res.writeHead(502); res.end("Bad Gateway"); });
360
- req.pipe(proxy);
250
+ emitCompleted();
251
+ onDone();
361
252
  }
362
253
 
363
- // ── Export ──
254
+ // Public server entry point.
364
255
 
365
- export function startAdapter(port = 4142) {
256
+ export function startAdapter(port = 2026) {
366
257
  const server = http.createServer((req, res) => {
367
- if (req.method === "POST" && req.url?.startsWith("/v1/responses")) {
258
+ const pathname = requestPath(req.url);
259
+
260
+ if (req.method === "POST" && pathname === "/v1/responses") {
368
261
  let body = "";
369
262
  req.on("data", (c) => (body += c));
370
263
  req.on("end", async () => {
371
264
  try {
372
265
  const parsed = JSON.parse(body);
266
+ const prepared = prepareResponsesRequest(parsed);
373
267
  const model = parsed.model || "unknown";
374
- console.log(`[codex-copilot-dx] ${model} stream=${!!parsed.stream}`);
268
+ console.log(status("info", `responses model=${model} stream=${!!parsed.stream}`));
375
269
  if (RESPONSES_ONLY.has(model)) {
376
- await proxyCopilotResponses(parsed, req, res);
270
+ await proxyCopilotResponses(prepared, req, res);
377
271
  } else {
378
- const chatReq = responsesToChat(parsed);
272
+ const chatReq = responsesToChat(prepared.body);
379
273
  if (parsed.stream) {
380
274
  forwardToChat(chatReq, (event, data) => {
381
275
  if (!res.headersSent) res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
382
276
  res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
277
+ if (event === "response.completed") rememberResponseHistory(prepared, data.response);
383
278
  }, () => { if (!res.writableEnded) res.end(); }, (status, errMsg) => { if (!res.headersSent) res.writeHead(status || 500); res.end(errMsg); });
384
279
  } else {
385
280
  chatReq.stream = false;
386
281
  delete chatReq.max_tokens;
387
- const reqBody = JSON.stringify(chatReq);
388
- const opts = { hostname: "localhost", port: COPILOT_API_PORT, path: "/v1/chat/completions", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(reqBody) } };
389
- const proxy = http.request(opts, (upRes) => {
390
- let data = ""; upRes.on("data", (c) => (data += c));
391
- upRes.on("end", () => { try { const resp = chatToResponses(JSON.parse(data), model); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(resp)); } catch { res.writeHead(500).end("Parse error"); } });
392
- });
393
- proxy.on("error", () => res.writeHead(502).end("Bad Gateway"));
394
- proxy.write(reqBody);
395
- proxy.end();
282
+ try {
283
+ const upstream = await chatCompletions({ ...chatReq, stream: false });
284
+ const data = await upstream.text();
285
+ const resp = chatToResponses(JSON.parse(data), model);
286
+ rememberResponseHistory(prepared, resp);
287
+ res.writeHead(200, { "Content-Type": "application/json" });
288
+ res.end(JSON.stringify(resp));
289
+ } catch (e) {
290
+ if (!res.headersSent) res.writeHead(502);
291
+ res.end("Bad Gateway");
292
+ }
293
+ }
294
+ }
295
+ } catch (e) {
296
+ console.error(status("err", `Responses request failed: ${e.message}`));
297
+ if (!res.headersSent) res.writeHead(400);
298
+ res.end(JSON.stringify({ error: e.message }));
299
+ }
300
+ });
301
+ return;
302
+ }
303
+
304
+ if (req.method === "POST" && pathname === "/v1/responses/compact") {
305
+ let body = "";
306
+ req.on("data", (c) => (body += c));
307
+ req.on("end", async () => {
308
+ try {
309
+ const parsed = JSON.parse(body);
310
+ const prepared = prepareResponsesRequest(parsed);
311
+ const model = parsed.model || "unknown";
312
+ console.log(status("info", `responses compact model=${model} stream=${!!parsed.stream}`));
313
+ await proxyCopilotResponses(prepared, req, res, copilotResponsesCompact);
314
+ } catch (e) {
315
+ console.error(status("err", `Responses compact request failed: ${e.message}`));
316
+ if (!res.headersSent) res.writeHead(400);
317
+ res.end(JSON.stringify({ error: e.message }));
318
+ }
319
+ });
320
+ return;
321
+ }
322
+
323
+ if (req.method === "GET" && pathname === "/v1/models") {
324
+ listModels()
325
+ .then(({ status, body }) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(body); })
326
+ .catch((e) => { if (!res.headersSent) res.writeHead(502); res.end(JSON.stringify({ error: e.message })); });
327
+ return;
328
+ }
329
+
330
+ if (req.method === "POST" && pathname === "/v1/messages/count_tokens") {
331
+ let body = "";
332
+ req.on("data", (c) => (body += c));
333
+ req.on("end", () => {
334
+ try {
335
+ const parsed = JSON.parse(body);
336
+ res.writeHead(200, { "Content-Type": "application/json" });
337
+ res.end(JSON.stringify(countTokens(parsed)));
338
+ } catch (e) {
339
+ if (!res.headersSent) res.writeHead(400);
340
+ res.end(JSON.stringify({ error: e.message }));
341
+ }
342
+ });
343
+ return;
344
+ }
345
+
346
+ if (req.method === "POST" && pathname === "/v1/messages") {
347
+ let body = "";
348
+ req.on("data", (c) => (body += c));
349
+ req.on("end", async () => {
350
+ try {
351
+ const parsed = JSON.parse(body);
352
+ const model = parsed.model || "unknown";
353
+ console.log(status("info", `messages model=${model} stream=${!!parsed.stream}`));
354
+ const chatReq = anthropicToChat(parsed);
355
+ if (parsed.stream) {
356
+ const upstream = await chatCompletions({ ...chatReq, stream: true });
357
+ if (!upstream.ok) {
358
+ if (!res.headersSent) res.writeHead(upstream.status);
359
+ res.end(await upstream.text());
360
+ return;
396
361
  }
362
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
363
+ await streamAnthropicFromLines(
364
+ webStreamLines(upstream),
365
+ (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`),
366
+ model,
367
+ );
368
+ if (!res.writableEnded) res.end();
369
+ } else {
370
+ const upstream = await chatCompletions({ ...chatReq, stream: false });
371
+ const data = await upstream.text();
372
+ const anthropicMsg = chatToAnthropic(JSON.parse(data), model);
373
+ res.writeHead(200, { "Content-Type": "application/json" });
374
+ res.end(JSON.stringify(anthropicMsg));
397
375
  }
398
376
  } catch (e) {
399
- console.error("[codex-copilot-dx] error:", e.message);
377
+ console.error(status("err", `Messages request failed: ${e.message}`));
400
378
  if (!res.headersSent) res.writeHead(400);
401
379
  res.end(JSON.stringify({ error: e.message }));
402
380
  }
403
381
  });
404
382
  return;
405
383
  }
406
- proxyPassthrough(req, res);
384
+
385
+ res.writeHead(404, { "Content-Type": "application/json" });
386
+ res.end(JSON.stringify({ error: "Not found" }));
407
387
  });
408
388
 
409
389
  server.on("upgrade", (req, socket) => {
@@ -418,7 +398,7 @@ export function startAdapter(port = 4142) {
418
398
 
419
399
  return new Promise((resolve) => {
420
400
  server.listen(port, () => {
421
- console.log(`[codex-copilot-dx] Adapter listening on http://localhost:${port}`);
401
+ console.log(status("ok", `Adapter listening on http://localhost:${port}`));
422
402
  resolve(server);
423
403
  });
424
404
  });