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/README.md +24 -11
- package/bin/cli.mjs +16 -16
- package/package.json +6 -2
- package/src/adapter.mjs +237 -257
- package/src/anthropic.mjs +267 -0
- package/src/auth.mjs +95 -0
- package/src/claude-config.mjs +56 -0
- package/src/config.mjs +77 -17
- package/src/copilot.mjs +316 -0
- package/src/launcher.mjs +8 -79
- package/src/status.mjs +11 -0
- package/src/stream.mjs +22 -0
package/src/adapter.mjs
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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
|
-
//
|
|
17
|
+
// Direct Copilot Responses API proxy.
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const RESPONSE_HISTORY_LIMIT = 64;
|
|
20
|
+
const responseHistories = new Map();
|
|
25
21
|
|
|
26
|
-
function
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
//
|
|
254
|
+
// Public server entry point.
|
|
364
255
|
|
|
365
|
-
export function startAdapter(port =
|
|
256
|
+
export function startAdapter(port = 2026) {
|
|
366
257
|
const server = http.createServer((req, res) => {
|
|
367
|
-
|
|
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(`
|
|
268
|
+
console.log(status("info", `responses model=${model} stream=${!!parsed.stream}`));
|
|
375
269
|
if (RESPONSES_ONLY.has(model)) {
|
|
376
|
-
await proxyCopilotResponses(
|
|
270
|
+
await proxyCopilotResponses(prepared, req, res);
|
|
377
271
|
} else {
|
|
378
|
-
const chatReq = responsesToChat(
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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("
|
|
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
|
-
|
|
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(`
|
|
401
|
+
console.log(status("ok", `Adapter listening on http://localhost:${port}`));
|
|
422
402
|
resolve(server);
|
|
423
403
|
});
|
|
424
404
|
});
|