codex-copilot-dx 0.4.4 → 0.4.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 +6 -1
- package/bin/cli.mjs +11 -2
- package/package.json +1 -1
- package/src/adapter.mjs +158 -49
- package/src/anthropic.mjs +17 -17
- package/src/claude-config.mjs +2 -2
- package/src/config.mjs +5 -5
- package/src/copilot.mjs +42 -19
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npx codex-copilot-dx@latest
|
|
|
28
28
|
On first run, it will:
|
|
29
29
|
1. Authenticate with GitHub via device flow (if needed)
|
|
30
30
|
2. Print the local package version and warn when a newer npm release is available
|
|
31
|
-
3. Start the adapter (
|
|
31
|
+
3. Start the adapter on loopback (`127.0.0.1:2026`)
|
|
32
32
|
4. Configure Codex (`~/.codex/config.toml`) to use the adapter, including stale shell env base URLs if present
|
|
33
33
|
5. Configure Claude Code (`~/.claude/settings.json`) to use the adapter; it creates the file when missing, otherwise backs up `settings.json.bak` before updating the local API env keys
|
|
34
34
|
6. Launch Codex Desktop
|
|
@@ -43,10 +43,15 @@ Environment variables:
|
|
|
43
43
|
|
|
44
44
|
| Variable | Default | Description |
|
|
45
45
|
|----------|---------|-------------|
|
|
46
|
+
| `ADAPTER_HOST` | `127.0.0.1` | Host for the adapter; set `0.0.0.0` only if you knowingly expose it on a trusted network |
|
|
46
47
|
| `ADAPTER_PORT` | `2026` | Port for the adapter |
|
|
48
|
+
| `CCDX_MAX_BODY_BYTES` | `134217728` | Maximum compressed/raw request body size |
|
|
49
|
+
| `CCDX_MAX_DECODED_BODY_BYTES` | `268435456` | Maximum decoded request body size after decompression |
|
|
50
|
+
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
|
|
47
51
|
| `CCDX_IMG_MAX_DIM` | `2048` | Max long edge in pixels for image downscaling |
|
|
48
52
|
| `CCDX_IMG_QUALITY` | `85` | WebP quality used when re-encoding images |
|
|
49
53
|
| `CCDX_IMG_MIN_BYTES` | `100000` | Images smaller than this are left untouched |
|
|
54
|
+
| `CCDX_IMG_CONCURRENCY` | `4` | Concurrent image optimization tasks; values above `12` are capped at `12` |
|
|
50
55
|
| `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
|
|
51
56
|
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
|
|
52
57
|
| `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
|
package/bin/cli.mjs
CHANGED
|
@@ -10,9 +10,14 @@ import { printUsageSummary } from "../src/usage.mjs";
|
|
|
10
10
|
import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
|
|
11
11
|
|
|
12
12
|
const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
|
|
13
|
+
const ADAPTER_HOST = process.env.ADAPTER_HOST || "127.0.0.1";
|
|
13
14
|
const LOCAL_VERSION = localPackageVersion();
|
|
14
15
|
const command = process.argv[2];
|
|
15
16
|
|
|
17
|
+
function isLoopbackHost(host) {
|
|
18
|
+
return ["127.0.0.1", "localhost", "::1"].includes(String(host).toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
17
22
|
console.log(`codex-copilot-dx v${LOCAL_VERSION}`);
|
|
18
23
|
process.exit(0);
|
|
@@ -41,8 +46,12 @@ try {
|
|
|
41
46
|
// Refresh the VS Code version in the background; fallback is non-blocking.
|
|
42
47
|
refreshVSCodeVersion();
|
|
43
48
|
|
|
49
|
+
if (!isLoopbackHost(ADAPTER_HOST)) {
|
|
50
|
+
console.log(status("warn", `ADAPTER_HOST=${ADAPTER_HOST} exposes the adapter beyond loopback. Use only on trusted networks.`));
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
// Start the in-process adapter.
|
|
45
|
-
await startAdapter(ADAPTER_PORT);
|
|
54
|
+
await startAdapter(ADAPTER_PORT, ADAPTER_HOST);
|
|
46
55
|
|
|
47
56
|
// Point Codex and Claude Code at the adapter.
|
|
48
57
|
ensureCodexConfig(ADAPTER_PORT);
|
|
@@ -54,7 +63,7 @@ try {
|
|
|
54
63
|
console.log(`
|
|
55
64
|
${status("ok", "Ready. Codex and Claude Code are using your GitHub Copilot subscription.")}
|
|
56
65
|
|
|
57
|
-
Adapter: http
|
|
66
|
+
Adapter: http://${ADAPTER_HOST}:${ADAPTER_PORT}
|
|
58
67
|
|
|
59
68
|
Press Ctrl+C to stop.
|
|
60
69
|
`);
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -21,11 +21,23 @@ const RESPONSES_ONLY = new Set([
|
|
|
21
21
|
|
|
22
22
|
const RESPONSE_HISTORY_LIMIT = 64;
|
|
23
23
|
const responseHistories = new Map();
|
|
24
|
+
const DEFAULT_MAX_BODY_BYTES = 128 * 1024 * 1024;
|
|
25
|
+
const DEFAULT_MAX_DECODED_BODY_BYTES = 256 * 1024 * 1024;
|
|
26
|
+
const DEFAULT_UPSTREAM_TIMEOUT_MS = 120 * 1000;
|
|
24
27
|
const gunzipAsync = promisify(zlib.gunzip);
|
|
25
28
|
const inflateAsync = promisify(zlib.inflate);
|
|
26
29
|
const brotliDecompressAsync = promisify(zlib.brotliDecompress);
|
|
27
30
|
const zstdDecompressAsync = zlib.zstdDecompress ? promisify(zlib.zstdDecompress) : null;
|
|
28
31
|
|
|
32
|
+
function positiveInt(value, fallback) {
|
|
33
|
+
const n = Number.parseInt(value, 10);
|
|
34
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MAX_BODY_BYTES = positiveInt(process.env.CCDX_MAX_BODY_BYTES, DEFAULT_MAX_BODY_BYTES);
|
|
38
|
+
const MAX_DECODED_BODY_BYTES = positiveInt(process.env.CCDX_MAX_DECODED_BODY_BYTES, DEFAULT_MAX_DECODED_BODY_BYTES);
|
|
39
|
+
const UPSTREAM_TIMEOUT_MS = positiveInt(process.env.CCDX_UPSTREAM_TIMEOUT_MS, DEFAULT_UPSTREAM_TIMEOUT_MS);
|
|
40
|
+
|
|
29
41
|
export function requestPath(reqUrl) {
|
|
30
42
|
return new URL(reqUrl || "/", "http://localhost").pathname;
|
|
31
43
|
}
|
|
@@ -34,15 +46,45 @@ function cloneJson(value) {
|
|
|
34
46
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
function httpError(message, statusCode) {
|
|
50
|
+
const err = new Error(message);
|
|
51
|
+
err.statusCode = statusCode;
|
|
52
|
+
return err;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function payloadTooLarge(kind, maxBytes) {
|
|
56
|
+
return httpError(`${kind} request body exceeds ${maxBytes} bytes`, 413);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readRequestBuffer(req, maxBytes) {
|
|
60
|
+
const contentLength = Number.parseInt(req.headers?.["content-length"] || "", 10);
|
|
61
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
62
|
+
throw payloadTooLarge("Raw", maxBytes);
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
const chunks = [];
|
|
66
|
+
let total = 0;
|
|
39
67
|
for await (const chunk of req) {
|
|
40
|
-
|
|
68
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
69
|
+
total += buf.length;
|
|
70
|
+
if (total > maxBytes) throw payloadTooLarge("Raw", maxBytes);
|
|
71
|
+
chunks.push(buf);
|
|
41
72
|
}
|
|
42
73
|
return Buffer.concat(chunks);
|
|
43
74
|
}
|
|
44
75
|
|
|
45
|
-
async function
|
|
76
|
+
async function decompressBody(decompress, buffer, maxBytes) {
|
|
77
|
+
try {
|
|
78
|
+
return await decompress(buffer, { maxOutputLength: maxBytes });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e?.code === "ERR_BUFFER_TOO_LARGE" || /maxOutputLength/i.test(e?.message || "")) {
|
|
81
|
+
throw payloadTooLarge("Decoded", maxBytes);
|
|
82
|
+
}
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function decodeRequestBuffer(buffer, contentEncoding, maxBytes) {
|
|
46
88
|
const encodings = String(contentEncoding || "identity")
|
|
47
89
|
.split(",")
|
|
48
90
|
.map((v) => v.trim().toLowerCase())
|
|
@@ -52,27 +94,79 @@ async function decodeRequestBuffer(buffer, contentEncoding) {
|
|
|
52
94
|
for (const encoding of encodings.reverse()) {
|
|
53
95
|
if (encoding === "identity") continue;
|
|
54
96
|
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
55
|
-
decoded = await gunzipAsync
|
|
97
|
+
decoded = await decompressBody(gunzipAsync, decoded, maxBytes);
|
|
56
98
|
} else if (encoding === "deflate") {
|
|
57
|
-
decoded = await inflateAsync
|
|
99
|
+
decoded = await decompressBody(inflateAsync, decoded, maxBytes);
|
|
58
100
|
} else if (encoding === "br") {
|
|
59
|
-
decoded = await brotliDecompressAsync
|
|
101
|
+
decoded = await decompressBody(brotliDecompressAsync, decoded, maxBytes);
|
|
60
102
|
} else if (encoding === "zstd") {
|
|
61
103
|
if (!zstdDecompressAsync) throw new Error("Unsupported Content-Encoding: zstd");
|
|
62
|
-
decoded = await zstdDecompressAsync
|
|
104
|
+
decoded = await decompressBody(zstdDecompressAsync, decoded, maxBytes);
|
|
63
105
|
} else {
|
|
64
106
|
throw new Error(`Unsupported Content-Encoding: ${encoding}`);
|
|
65
107
|
}
|
|
108
|
+
if (decoded.length > maxBytes) throw payloadTooLarge("Decoded", maxBytes);
|
|
66
109
|
}
|
|
110
|
+
if (decoded.length > maxBytes) throw payloadTooLarge("Decoded", maxBytes);
|
|
67
111
|
return decoded;
|
|
68
112
|
}
|
|
69
113
|
|
|
70
|
-
export async function readJsonBody(req
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
export async function readJsonBody(req, {
|
|
115
|
+
maxBodyBytes = MAX_BODY_BYTES,
|
|
116
|
+
maxDecodedBodyBytes = MAX_DECODED_BODY_BYTES,
|
|
117
|
+
} = {}) {
|
|
118
|
+
const buffer = await readRequestBuffer(req, maxBodyBytes);
|
|
119
|
+
const decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], maxDecodedBodyBytes);
|
|
73
120
|
return JSON.parse(decoded.toString("utf8"));
|
|
74
121
|
}
|
|
75
122
|
|
|
123
|
+
function sendJsonError(res, err, fallbackStatus = 400) {
|
|
124
|
+
if (!res.headersSent) res.writeHead(err?.statusCode || fallbackStatus, { "Content-Type": "application/json" });
|
|
125
|
+
res.end(JSON.stringify({ error: err?.message || "Request failed" }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function writeOrDrain(res, chunk) {
|
|
129
|
+
if (res.destroyed || res.writableEnded) return Promise.resolve(false);
|
|
130
|
+
if (res.write(chunk)) return Promise.resolve(true);
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const cleanup = () => {
|
|
133
|
+
res.off("drain", onDrain);
|
|
134
|
+
res.off("error", onError);
|
|
135
|
+
res.off("close", onClose);
|
|
136
|
+
};
|
|
137
|
+
const onDrain = () => { cleanup(); resolve(true); };
|
|
138
|
+
const onClose = () => { cleanup(); resolve(false); };
|
|
139
|
+
const onError = (err) => { cleanup(); reject(err); };
|
|
140
|
+
res.once("drain", onDrain);
|
|
141
|
+
res.once("close", onClose);
|
|
142
|
+
res.once("error", onError);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createRequestAbort(req, res) {
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
let timer = null;
|
|
149
|
+
let cleaned = false;
|
|
150
|
+
const abort = () => {
|
|
151
|
+
if (!cleaned && !controller.signal.aborted) controller.abort();
|
|
152
|
+
};
|
|
153
|
+
req.on("aborted", abort);
|
|
154
|
+
res.on("close", abort);
|
|
155
|
+
return {
|
|
156
|
+
signal: controller.signal,
|
|
157
|
+
setTimeout(ms) {
|
|
158
|
+
if (timer) clearTimeout(timer);
|
|
159
|
+
if (ms > 0) timer = setTimeout(abort, ms);
|
|
160
|
+
},
|
|
161
|
+
cleanup() {
|
|
162
|
+
cleaned = true;
|
|
163
|
+
if (timer) clearTimeout(timer);
|
|
164
|
+
req.off("aborted", abort);
|
|
165
|
+
res.off("close", abort);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
76
170
|
function responsesInputItems(input) {
|
|
77
171
|
if (input === undefined || input === null) return [];
|
|
78
172
|
if (typeof input === "string") {
|
|
@@ -161,8 +255,8 @@ function readSseEvents(buffer, onData) {
|
|
|
161
255
|
}
|
|
162
256
|
}
|
|
163
257
|
|
|
164
|
-
async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses) {
|
|
165
|
-
const resp = await upstream(reqContext.body);
|
|
258
|
+
async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
|
|
259
|
+
const resp = await upstream(reqContext.body, { signal: options.signal });
|
|
166
260
|
|
|
167
261
|
if (reqContext.body.stream) {
|
|
168
262
|
res.writeHead(resp.status, {
|
|
@@ -183,7 +277,7 @@ async function proxyCopilotResponses(reqContext, req, res, upstream = copilotRes
|
|
|
183
277
|
}
|
|
184
278
|
buffer += decoder.decode(value, { stream: true });
|
|
185
279
|
buffer = readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
|
|
186
|
-
res
|
|
280
|
+
await writeOrDrain(res, value);
|
|
187
281
|
}
|
|
188
282
|
} else {
|
|
189
283
|
const data = await resp.text();
|
|
@@ -260,11 +354,11 @@ function chatToResponses(chatResp, model) {
|
|
|
260
354
|
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 };
|
|
261
355
|
}
|
|
262
356
|
|
|
263
|
-
async function forwardToChat(chatReq, emitEvent, onDone, onError) {
|
|
357
|
+
async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
|
|
264
358
|
delete chatReq.max_tokens;
|
|
265
359
|
let resp;
|
|
266
360
|
try {
|
|
267
|
-
resp = await chatCompletions({ ...chatReq, stream: true });
|
|
361
|
+
resp = await chatCompletions({ ...chatReq, stream: true }, { signal: options.signal });
|
|
268
362
|
} catch (e) {
|
|
269
363
|
onError(502, e.message);
|
|
270
364
|
return;
|
|
@@ -276,37 +370,37 @@ async function forwardToChat(chatReq, emitEvent, onDone, onError) {
|
|
|
276
370
|
const respId = `resp_${uid()}`, itemId = `item_${uid()}`;
|
|
277
371
|
let actualModel = "unknown", fullText = "", toolCalls = {}, hasToolCalls = false;
|
|
278
372
|
|
|
279
|
-
emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
280
|
-
emitEvent("response.output_item.added", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "in_progress", content: [] } });
|
|
281
|
-
emitEvent("response.content_part.added", { output_index: 0, content_index: 0, part: { type: "output_text", text: "" } });
|
|
373
|
+
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
374
|
+
await emitEvent("response.output_item.added", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "in_progress", content: [] } });
|
|
375
|
+
await emitEvent("response.content_part.added", { output_index: 0, content_index: 0, part: { type: "output_text", text: "" } });
|
|
282
376
|
|
|
283
|
-
const emitCompleted = () => {
|
|
377
|
+
const emitCompleted = async () => {
|
|
284
378
|
if (!hasToolCalls) {
|
|
285
|
-
emitEvent("response.output_text.done", { output_index: 0, content_index: 0, text: fullText });
|
|
286
|
-
emitEvent("response.output_item.done", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] } });
|
|
379
|
+
await emitEvent("response.output_text.done", { output_index: 0, content_index: 0, text: fullText });
|
|
380
|
+
await emitEvent("response.output_item.done", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] } });
|
|
287
381
|
}
|
|
288
382
|
const output = hasToolCalls
|
|
289
383
|
? Object.entries(toolCalls).map(([id, tc]) => ({ type: "function_call", id, call_id: id, name: tc.name, arguments: tc.arguments, status: "completed" }))
|
|
290
384
|
: [{ type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] }];
|
|
291
|
-
emitEvent("response.completed", { response: { id: respId, object: "response", status: "completed", model: actualModel, output, usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } } });
|
|
385
|
+
await emitEvent("response.completed", { response: { id: respId, object: "response", status: "completed", model: actualModel, output, usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } } });
|
|
292
386
|
};
|
|
293
387
|
|
|
294
388
|
try {
|
|
295
389
|
for await (const line of webStreamLines(resp)) {
|
|
296
390
|
if (!line.startsWith("data: ")) continue;
|
|
297
391
|
const data = line.slice(6).trim();
|
|
298
|
-
if (data === "[DONE]") { emitCompleted(); onDone(); return; }
|
|
392
|
+
if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
|
|
299
393
|
let parsed;
|
|
300
394
|
try { parsed = JSON.parse(data); } catch { continue; }
|
|
301
395
|
if (parsed.model) actualModel = parsed.model;
|
|
302
396
|
const delta = parsed.choices?.[0]?.delta;
|
|
303
397
|
if (!delta) continue;
|
|
304
|
-
if (delta.content) { fullText += delta.content; emitEvent("response.output_text.delta", { output_index: 0, content_index: 0, delta: delta.content }); }
|
|
398
|
+
if (delta.content) { fullText += delta.content; await emitEvent("response.output_text.delta", { output_index: 0, content_index: 0, delta: delta.content }); }
|
|
305
399
|
if (delta.tool_calls) {
|
|
306
400
|
hasToolCalls = true;
|
|
307
401
|
for (const tc of delta.tool_calls) {
|
|
308
402
|
const id = tc.id || Object.keys(toolCalls)[tc.index] || `call_${tc.index}`;
|
|
309
|
-
if (!toolCalls[id]) { toolCalls[id] = { name: "", arguments: "" }; emitEvent("response.output_item.added", { output_index: Object.keys(toolCalls).length - 1, item: { type: "function_call", id, call_id: id, name: tc.function?.name || "", arguments: "", status: "in_progress" } }); }
|
|
403
|
+
if (!toolCalls[id]) { toolCalls[id] = { name: "", arguments: "" }; await emitEvent("response.output_item.added", { output_index: Object.keys(toolCalls).length - 1, item: { type: "function_call", id, call_id: id, name: tc.function?.name || "", arguments: "", status: "in_progress" } }); }
|
|
310
404
|
if (tc.function?.name) toolCalls[id].name += tc.function.name;
|
|
311
405
|
if (tc.function?.arguments) toolCalls[id].arguments += tc.function.arguments;
|
|
312
406
|
}
|
|
@@ -316,42 +410,44 @@ async function forwardToChat(chatReq, emitEvent, onDone, onError) {
|
|
|
316
410
|
onError(500, e?.message || "upstream stream error");
|
|
317
411
|
return;
|
|
318
412
|
}
|
|
319
|
-
emitCompleted();
|
|
413
|
+
await emitCompleted();
|
|
320
414
|
onDone();
|
|
321
415
|
}
|
|
322
416
|
|
|
323
417
|
// Public server entry point.
|
|
324
418
|
|
|
325
|
-
export function startAdapter(port = 2026) {
|
|
419
|
+
export function startAdapter(port = 2026, host = "127.0.0.1") {
|
|
326
420
|
const server = http.createServer((req, res) => {
|
|
327
421
|
const pathname = requestPath(req.url);
|
|
328
422
|
|
|
329
423
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
330
424
|
(async () => {
|
|
425
|
+
const abort = createRequestAbort(req, res);
|
|
331
426
|
try {
|
|
332
427
|
const parsed = await readJsonBody(req);
|
|
428
|
+
if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
333
429
|
const prepared = prepareResponsesRequest(parsed);
|
|
334
430
|
prepared.surface = "responses";
|
|
335
431
|
const model = parsed.model || "unknown";
|
|
336
432
|
console.log(status("info", `responses model=${model} stream=${!!parsed.stream}`));
|
|
337
433
|
if (RESPONSES_ONLY.has(model)) {
|
|
338
|
-
await proxyCopilotResponses(prepared, req, res);
|
|
434
|
+
await proxyCopilotResponses(prepared, req, res, copilotResponses, { signal: abort.signal });
|
|
339
435
|
} else {
|
|
340
436
|
const chatReq = responsesToChat(prepared.body);
|
|
341
437
|
if (parsed.stream) {
|
|
342
|
-
forwardToChat(chatReq, (event, data) => {
|
|
438
|
+
await forwardToChat(chatReq, async (event, data) => {
|
|
343
439
|
if (!res.headersSent) res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
344
|
-
res
|
|
440
|
+
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
345
441
|
if (event === "response.completed") {
|
|
346
442
|
rememberResponseHistory(prepared, data.response);
|
|
347
443
|
recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
|
|
348
444
|
}
|
|
349
|
-
}, () => { if (!res.writableEnded) res.end(); }, (status, errMsg) => { if (!res.headersSent) res.writeHead(status || 500); res.end(errMsg); });
|
|
445
|
+
}, () => { if (!res.writableEnded) res.end(); }, (status, errMsg) => { if (!res.headersSent) res.writeHead(status || 500); res.end(errMsg); }, { signal: abort.signal });
|
|
350
446
|
} else {
|
|
351
447
|
chatReq.stream = false;
|
|
352
448
|
delete chatReq.max_tokens;
|
|
353
449
|
try {
|
|
354
|
-
const upstream = await chatCompletions({ ...chatReq, stream: false });
|
|
450
|
+
const upstream = await chatCompletions({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
355
451
|
const data = await upstream.text();
|
|
356
452
|
const resp = chatToResponses(JSON.parse(data), model);
|
|
357
453
|
rememberResponseHistory(prepared, resp);
|
|
@@ -366,8 +462,9 @@ export function startAdapter(port = 2026) {
|
|
|
366
462
|
}
|
|
367
463
|
} catch (e) {
|
|
368
464
|
console.error(status("err", `Responses request failed: ${e.message}`));
|
|
369
|
-
|
|
370
|
-
|
|
465
|
+
sendJsonError(res, e);
|
|
466
|
+
} finally {
|
|
467
|
+
abort.cleanup();
|
|
371
468
|
}
|
|
372
469
|
})();
|
|
373
470
|
return;
|
|
@@ -375,38 +472,46 @@ export function startAdapter(port = 2026) {
|
|
|
375
472
|
|
|
376
473
|
if (req.method === "POST" && pathname === "/v1/responses/compact") {
|
|
377
474
|
(async () => {
|
|
475
|
+
const abort = createRequestAbort(req, res);
|
|
378
476
|
try {
|
|
379
477
|
const parsed = await readJsonBody(req);
|
|
478
|
+
if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
380
479
|
const prepared = prepareResponsesRequest(parsed);
|
|
381
480
|
prepared.surface = "responses_compact";
|
|
382
481
|
const model = parsed.model || "unknown";
|
|
383
482
|
console.log(status("info", `responses compact model=${model} stream=${!!parsed.stream}`));
|
|
384
|
-
await proxyCopilotResponses(prepared, req, res, copilotResponsesCompact);
|
|
483
|
+
await proxyCopilotResponses(prepared, req, res, copilotResponsesCompact, { signal: abort.signal });
|
|
385
484
|
} catch (e) {
|
|
386
485
|
console.error(status("err", `Responses compact request failed: ${e.message}`));
|
|
387
|
-
|
|
388
|
-
|
|
486
|
+
sendJsonError(res, e);
|
|
487
|
+
} finally {
|
|
488
|
+
abort.cleanup();
|
|
389
489
|
}
|
|
390
490
|
})();
|
|
391
491
|
return;
|
|
392
492
|
}
|
|
393
493
|
|
|
394
494
|
if (req.method === "GET" && pathname === "/v1/models") {
|
|
395
|
-
|
|
495
|
+
const abort = createRequestAbort(req, res);
|
|
496
|
+
abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
497
|
+
listModels({ signal: abort.signal })
|
|
396
498
|
.then(({ status, body }) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(body); })
|
|
397
|
-
.catch((e) => { if (!res.headersSent) res.writeHead(502); res.end(JSON.stringify({ error: e.message })); })
|
|
499
|
+
.catch((e) => { if (!res.headersSent) res.writeHead(502); res.end(JSON.stringify({ error: e.message })); })
|
|
500
|
+
.finally(() => abort.cleanup());
|
|
398
501
|
return;
|
|
399
502
|
}
|
|
400
503
|
|
|
401
504
|
if (req.method === "POST" && pathname === "/v1/messages/count_tokens") {
|
|
402
505
|
(async () => {
|
|
506
|
+
const abort = createRequestAbort(req, res);
|
|
403
507
|
try {
|
|
404
508
|
const parsed = await readJsonBody(req);
|
|
405
509
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
406
510
|
res.end(JSON.stringify(countTokens(parsed)));
|
|
407
511
|
} catch (e) {
|
|
408
|
-
|
|
409
|
-
|
|
512
|
+
sendJsonError(res, e);
|
|
513
|
+
} finally {
|
|
514
|
+
abort.cleanup();
|
|
410
515
|
}
|
|
411
516
|
})();
|
|
412
517
|
return;
|
|
@@ -414,13 +519,15 @@ export function startAdapter(port = 2026) {
|
|
|
414
519
|
|
|
415
520
|
if (req.method === "POST" && pathname === "/v1/messages") {
|
|
416
521
|
(async () => {
|
|
522
|
+
const abort = createRequestAbort(req, res);
|
|
417
523
|
try {
|
|
418
524
|
const parsed = await readJsonBody(req);
|
|
525
|
+
if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
419
526
|
const model = parsed.model || "unknown";
|
|
420
527
|
console.log(status("info", `messages model=${model} stream=${!!parsed.stream}`));
|
|
421
528
|
const chatReq = anthropicToChat(parsed);
|
|
422
529
|
if (parsed.stream) {
|
|
423
|
-
const upstream = await chatCompletions({ ...chatReq, stream: true });
|
|
530
|
+
const upstream = await chatCompletions({ ...chatReq, stream: true }, { signal: abort.signal });
|
|
424
531
|
if (!upstream.ok) {
|
|
425
532
|
if (!res.headersSent) res.writeHead(upstream.status);
|
|
426
533
|
res.end(await upstream.text());
|
|
@@ -431,17 +538,17 @@ export function startAdapter(port = 2026) {
|
|
|
431
538
|
let usage;
|
|
432
539
|
await streamAnthropicFromLines(
|
|
433
540
|
webStreamLines(upstream),
|
|
434
|
-
(event, data) => {
|
|
541
|
+
async (event, data) => {
|
|
435
542
|
if (event === "message_start") messageId = data.message?.id;
|
|
436
543
|
if (event === "message_delta") usage = data.usage;
|
|
437
|
-
res
|
|
544
|
+
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
438
545
|
},
|
|
439
546
|
model,
|
|
440
547
|
);
|
|
441
548
|
recordAnthropicUsage({ surface: "messages", mode: "stream", model, responseId: messageId, usage });
|
|
442
549
|
if (!res.writableEnded) res.end();
|
|
443
550
|
} else {
|
|
444
|
-
const upstream = await chatCompletions({ ...chatReq, stream: false });
|
|
551
|
+
const upstream = await chatCompletions({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
445
552
|
const data = await upstream.text();
|
|
446
553
|
const anthropicMsg = chatToAnthropic(JSON.parse(data), model);
|
|
447
554
|
recordAnthropicUsage({ surface: "messages", mode: "json", model: anthropicMsg.model, responseId: anthropicMsg.id, usage: anthropicMsg.usage });
|
|
@@ -450,8 +557,9 @@ export function startAdapter(port = 2026) {
|
|
|
450
557
|
}
|
|
451
558
|
} catch (e) {
|
|
452
559
|
console.error(status("err", `Messages request failed: ${e.message}`));
|
|
453
|
-
|
|
454
|
-
|
|
560
|
+
sendJsonError(res, e);
|
|
561
|
+
} finally {
|
|
562
|
+
abort.cleanup();
|
|
455
563
|
}
|
|
456
564
|
})();
|
|
457
565
|
return;
|
|
@@ -472,8 +580,9 @@ export function startAdapter(port = 2026) {
|
|
|
472
580
|
});
|
|
473
581
|
|
|
474
582
|
return new Promise((resolve) => {
|
|
475
|
-
server.listen(port, () => {
|
|
476
|
-
|
|
583
|
+
server.listen(port, host, () => {
|
|
584
|
+
const actualPort = server.address()?.port || port;
|
|
585
|
+
console.log(status("ok", `Adapter listening on http://${host}:${actualPort}`));
|
|
477
586
|
resolve(server);
|
|
478
587
|
});
|
|
479
588
|
});
|
package/src/anthropic.mjs
CHANGED
|
@@ -163,26 +163,26 @@ export async function streamAnthropicFromLines(lineIterator, emit, model) {
|
|
|
163
163
|
let sawToolUse = false;
|
|
164
164
|
let outputTokens = 0;
|
|
165
165
|
|
|
166
|
-
const ensureStart = () => {
|
|
166
|
+
const ensureStart = async () => {
|
|
167
167
|
if (started) return;
|
|
168
168
|
started = true;
|
|
169
|
-
emit("message_start", { type: "message_start", message: {
|
|
169
|
+
await emit("message_start", { type: "message_start", message: {
|
|
170
170
|
id: msgId, type: "message", role: "assistant", model: actualModel,
|
|
171
171
|
content: [], stop_reason: null, stop_sequence: null,
|
|
172
172
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
173
173
|
} });
|
|
174
174
|
};
|
|
175
175
|
|
|
176
|
-
const openText = () => {
|
|
176
|
+
const openText = async () => {
|
|
177
177
|
if (textOpen) return;
|
|
178
178
|
blockIndex += 1;
|
|
179
179
|
textOpen = true;
|
|
180
|
-
emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
180
|
+
await emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
181
181
|
content_block: { type: "text", text: "" } });
|
|
182
182
|
};
|
|
183
|
-
const closeText = () => {
|
|
183
|
+
const closeText = async () => {
|
|
184
184
|
if (!textOpen) return;
|
|
185
|
-
emit("content_block_stop", { type: "content_block_stop", index: blockIndex });
|
|
185
|
+
await emit("content_block_stop", { type: "content_block_stop", index: blockIndex });
|
|
186
186
|
textOpen = false;
|
|
187
187
|
};
|
|
188
188
|
|
|
@@ -197,27 +197,27 @@ export async function streamAnthropicFromLines(lineIterator, emit, model) {
|
|
|
197
197
|
const choice = parsed.choices?.[0];
|
|
198
198
|
if (!choice) continue;
|
|
199
199
|
const delta = choice.delta || {};
|
|
200
|
-
ensureStart();
|
|
200
|
+
await ensureStart();
|
|
201
201
|
|
|
202
202
|
if (delta.content && !sawToolUse) {
|
|
203
|
-
openText();
|
|
204
|
-
emit("content_block_delta", { type: "content_block_delta", index: blockIndex,
|
|
203
|
+
await openText();
|
|
204
|
+
await emit("content_block_delta", { type: "content_block_delta", index: blockIndex,
|
|
205
205
|
delta: { type: "text_delta", text: delta.content } });
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
if (Array.isArray(delta.tool_calls)) {
|
|
209
209
|
sawToolUse = true;
|
|
210
|
-
closeText();
|
|
210
|
+
await closeText();
|
|
211
211
|
for (const tc of delta.tool_calls) {
|
|
212
212
|
const oi = tc.index ?? 0;
|
|
213
213
|
if (!toolBlocks[oi]) {
|
|
214
214
|
blockIndex += 1;
|
|
215
215
|
toolBlocks[oi] = { anthropicIndex: blockIndex };
|
|
216
|
-
emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
216
|
+
await emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
217
217
|
content_block: { type: "tool_use", id: tc.id || `tu_${uid()}`, name: tc.function?.name || "", input: {} } });
|
|
218
218
|
}
|
|
219
219
|
if (tc.function?.arguments) {
|
|
220
|
-
emit("content_block_delta", { type: "content_block_delta", index: toolBlocks[oi].anthropicIndex,
|
|
220
|
+
await emit("content_block_delta", { type: "content_block_delta", index: toolBlocks[oi].anthropicIndex,
|
|
221
221
|
delta: { type: "input_json_delta", partial_json: tc.function.arguments } });
|
|
222
222
|
}
|
|
223
223
|
}
|
|
@@ -226,16 +226,16 @@ export async function streamAnthropicFromLines(lineIterator, emit, model) {
|
|
|
226
226
|
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
ensureStart();
|
|
230
|
-
if (textOpen) closeText();
|
|
229
|
+
await ensureStart();
|
|
230
|
+
if (textOpen) await closeText();
|
|
231
231
|
for (const oi of Object.keys(toolBlocks)) {
|
|
232
|
-
emit("content_block_stop", { type: "content_block_stop", index: toolBlocks[oi].anthropicIndex });
|
|
232
|
+
await emit("content_block_stop", { type: "content_block_stop", index: toolBlocks[oi].anthropicIndex });
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
const stop_reason = finishReason ? mapStopReason(finishReason) : (sawToolUse ? "tool_use" : "end_turn");
|
|
236
|
-
emit("message_delta", { type: "message_delta", delta: { stop_reason, stop_sequence: null },
|
|
236
|
+
await emit("message_delta", { type: "message_delta", delta: { stop_reason, stop_sequence: null },
|
|
237
237
|
usage: { output_tokens: outputTokens } });
|
|
238
|
-
emit("message_stop", { type: "message_stop" });
|
|
238
|
+
await emit("message_stop", { type: "message_stop" });
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
// Copilot has no count_tokens endpoint; estimate locally with gpt-tokenizer.
|
package/src/claude-config.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
|
7
7
|
|
|
8
8
|
// Return an updated settings object without mutating the input.
|
|
9
9
|
export function computeUpdatedSettings(settings, port) {
|
|
10
|
-
const target = `http://
|
|
10
|
+
const target = `http://127.0.0.1:${port}`;
|
|
11
11
|
const current = settings?.env?.ANTHROPIC_BASE_URL;
|
|
12
12
|
const currentToken = settings?.env?.ANTHROPIC_AUTH_TOKEN;
|
|
13
13
|
const changed = current !== target || currentToken !== "dummy";
|
|
@@ -24,7 +24,7 @@ export function computeUpdatedSettings(settings, port) {
|
|
|
24
24
|
|
|
25
25
|
// Point Claude Code at the adapter by updating ~/.claude/settings.json.
|
|
26
26
|
export function ensureClaudeConfig(port = 2026) {
|
|
27
|
-
const target = `http://
|
|
27
|
+
const target = `http://127.0.0.1:${port}`;
|
|
28
28
|
|
|
29
29
|
if (!fs.existsSync(SETTINGS_PATH)) {
|
|
30
30
|
fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
|
package/src/config.mjs
CHANGED
|
@@ -33,8 +33,8 @@ function setTomlKey(lines, sectionName, key, value) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function computeUpdatedCodexConfig(content, adapterPort = 2026) {
|
|
36
|
-
const baseUrl = `http://
|
|
37
|
-
const anthropicBaseUrl = `http://
|
|
36
|
+
const baseUrl = `http://127.0.0.1:${adapterPort}/v1`;
|
|
37
|
+
const anthropicBaseUrl = `http://127.0.0.1:${adapterPort}`;
|
|
38
38
|
const hadTrailingNewline = content.endsWith("\n");
|
|
39
39
|
const lines = content.split("\n");
|
|
40
40
|
if (hadTrailingNewline) lines.pop();
|
|
@@ -59,8 +59,8 @@ export function computeUpdatedCodexConfig(content, adapterPort = 2026) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function initialCodexConfig(adapterPort) {
|
|
62
|
-
const baseUrl = `http://
|
|
63
|
-
const anthropicBaseUrl = `http://
|
|
62
|
+
const baseUrl = `http://127.0.0.1:${adapterPort}/v1`;
|
|
63
|
+
const anthropicBaseUrl = `http://127.0.0.1:${adapterPort}`;
|
|
64
64
|
return `openai_base_url = "${baseUrl}"
|
|
65
65
|
|
|
66
66
|
[shell_environment_policy]
|
|
@@ -75,7 +75,7 @@ OPENAI_API_KEY = "dummy"
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export function ensureCodexConfig(adapterPort = 2026) {
|
|
78
|
-
const baseUrl = `http://
|
|
78
|
+
const baseUrl = `http://127.0.0.1:${adapterPort}/v1`;
|
|
79
79
|
|
|
80
80
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
81
81
|
// Codex config does not exist yet; create the local proxy defaults.
|
package/src/copilot.mjs
CHANGED
|
@@ -10,9 +10,17 @@ let apiBase = DEFAULT_API_BASE;
|
|
|
10
10
|
const IMG_MAX_DIM = parseInt(process.env.CCDX_IMG_MAX_DIM || "2048", 10);
|
|
11
11
|
const IMG_QUALITY = parseInt(process.env.CCDX_IMG_QUALITY || "85", 10);
|
|
12
12
|
const IMG_MIN_BYTES = parseInt(process.env.CCDX_IMG_MIN_BYTES || "100000", 10);
|
|
13
|
+
const DEFAULT_IMG_CONCURRENCY = 4;
|
|
14
|
+
const IMG_MAX_CONCURRENCY = 12;
|
|
13
15
|
const IMG_OPT_DISABLED = process.env.CCDX_DISABLE_IMG_OPT === "1";
|
|
16
|
+
const IMG_CONCURRENCY = parseImageConcurrency(process.env.CCDX_IMG_CONCURRENCY);
|
|
14
17
|
let sharpImport = null;
|
|
15
18
|
|
|
19
|
+
export function parseImageConcurrency(value) {
|
|
20
|
+
const n = Number.parseInt(value, 10);
|
|
21
|
+
return Number.isFinite(n) && n > 0 ? Math.min(n, IMG_MAX_CONCURRENCY) : DEFAULT_IMG_CONCURRENCY;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
export function parseApiBase(data) {
|
|
17
25
|
return (data && data.endpoints && typeof data.endpoints.api === "string" && data.endpoints.api)
|
|
18
26
|
? data.endpoints.api
|
|
@@ -59,14 +67,26 @@ export async function optimizeImageDataUrl(dataUrl) {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
export async function runWithConcurrency(taskFns, concurrency) {
|
|
71
|
+
if (!Array.isArray(taskFns) || taskFns.length === 0) return;
|
|
72
|
+
const limit = Math.min(parseImageConcurrency(concurrency), taskFns.length);
|
|
73
|
+
let next = 0;
|
|
74
|
+
await Promise.all(Array.from({ length: limit }, async () => {
|
|
75
|
+
while (next < taskFns.length) {
|
|
76
|
+
const task = taskFns[next++];
|
|
77
|
+
await task();
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
function visitImageParts(parts, tasks) {
|
|
63
83
|
if (!Array.isArray(parts)) return;
|
|
64
84
|
for (const part of parts) {
|
|
65
85
|
if (!part) continue;
|
|
66
86
|
if (part.type === "input_image" && typeof part.image_url === "string") {
|
|
67
|
-
tasks.push(
|
|
87
|
+
tasks.push(async () => { part.image_url = await optimizeImageDataUrl(part.image_url); });
|
|
68
88
|
} else if (part.type === "image" && part.source?.type === "base64" && part.source?.data) {
|
|
69
|
-
tasks.push(
|
|
89
|
+
tasks.push(async () => {
|
|
70
90
|
const dataUrl = `data:${part.source.media_type || "image/png"};base64,${part.source.data}`;
|
|
71
91
|
const opt = await optimizeImageDataUrl(dataUrl);
|
|
72
92
|
const match = /^data:([^;]+);base64,(.+)$/.exec(opt);
|
|
@@ -74,7 +94,7 @@ function visitImageParts(parts, tasks) {
|
|
|
74
94
|
part.source.media_type = match[1];
|
|
75
95
|
part.source.data = match[2];
|
|
76
96
|
}
|
|
77
|
-
})
|
|
97
|
+
});
|
|
78
98
|
}
|
|
79
99
|
}
|
|
80
100
|
}
|
|
@@ -97,13 +117,13 @@ export async function optimizeImagesInBody(reqBody) {
|
|
|
97
117
|
try {
|
|
98
118
|
const parsed = JSON.parse(trimmed);
|
|
99
119
|
if (Array.isArray(parsed)) {
|
|
100
|
-
const
|
|
101
|
-
visitImageParts(parsed,
|
|
102
|
-
const localTasks = tasks.slice(before);
|
|
120
|
+
const localTasks = [];
|
|
121
|
+
visitImageParts(parsed, localTasks);
|
|
103
122
|
if (localTasks.length) {
|
|
104
|
-
tasks.push(
|
|
123
|
+
tasks.push(async () => {
|
|
124
|
+
await runWithConcurrency(localTasks, IMG_CONCURRENCY);
|
|
105
125
|
item.output = JSON.stringify(parsed);
|
|
106
|
-
})
|
|
126
|
+
});
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
} catch {
|
|
@@ -114,7 +134,7 @@ export async function optimizeImagesInBody(reqBody) {
|
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
136
|
|
|
117
|
-
if (tasks.length) await
|
|
137
|
+
if (tasks.length) await runWithConcurrency(tasks, IMG_CONCURRENCY);
|
|
118
138
|
return reqBody;
|
|
119
139
|
}
|
|
120
140
|
|
|
@@ -235,11 +255,12 @@ function getGithubToken() {
|
|
|
235
255
|
return fs.readFileSync(GITHUB_TOKEN_PATH, "utf-8").trim();
|
|
236
256
|
}
|
|
237
257
|
|
|
238
|
-
export async function getCopilotToken() {
|
|
258
|
+
export async function getCopilotToken({ signal } = {}) {
|
|
239
259
|
if (copilotToken && Date.now() < copilotTokenExpiry - 60000) return copilotToken;
|
|
240
260
|
const ghToken = getGithubToken();
|
|
241
261
|
const resp = await fetch(`${GITHUB_API}/copilot_internal/v2/token`, {
|
|
242
262
|
headers: { Authorization: `token ${ghToken}`, Accept: "application/json" },
|
|
263
|
+
signal,
|
|
243
264
|
});
|
|
244
265
|
if (!resp.ok) throw new Error(`Failed to get Copilot token: ${resp.status}`);
|
|
245
266
|
const data = await resp.json();
|
|
@@ -254,8 +275,8 @@ export async function getCopilotToken() {
|
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
// chatReq is already converted by the adapter. The caller parses the raw Response.
|
|
257
|
-
export async function chatCompletions(chatReq) {
|
|
258
|
-
const token = await getCopilotToken();
|
|
278
|
+
export async function chatCompletions(chatReq, { signal } = {}) {
|
|
279
|
+
const token = await getCopilotToken({ signal });
|
|
259
280
|
const messages = chatReq.messages || [];
|
|
260
281
|
const headers = buildHeaders({
|
|
261
282
|
token,
|
|
@@ -267,21 +288,22 @@ export async function chatCompletions(chatReq) {
|
|
|
267
288
|
method: "POST",
|
|
268
289
|
headers,
|
|
269
290
|
body: JSON.stringify(chatReq),
|
|
291
|
+
signal,
|
|
270
292
|
});
|
|
271
293
|
}
|
|
272
294
|
|
|
273
|
-
export async function listModels() {
|
|
274
|
-
const token = await getCopilotToken();
|
|
295
|
+
export async function listModels({ signal } = {}) {
|
|
296
|
+
const token = await getCopilotToken({ signal });
|
|
275
297
|
const headers = buildHeaders({
|
|
276
298
|
token, version: getVSCodeVersion(), initiator: "user", vision: false,
|
|
277
299
|
});
|
|
278
|
-
const resp = await fetch(`${getApiBase()}/models`, { headers });
|
|
300
|
+
const resp = await fetch(`${getApiBase()}/models`, { headers, signal });
|
|
279
301
|
return { status: resp.status, body: await resp.text() };
|
|
280
302
|
}
|
|
281
303
|
|
|
282
304
|
// Responses-only models go directly to Copilot's /responses endpoint.
|
|
283
|
-
export async function responses(reqBody) {
|
|
284
|
-
const token = await getCopilotToken();
|
|
305
|
+
export async function responses(reqBody, { signal } = {}) {
|
|
306
|
+
const token = await getCopilotToken({ signal });
|
|
285
307
|
const beforeBytes = Buffer.byteLength(JSON.stringify(reqBody));
|
|
286
308
|
await optimizeImagesInBody(reqBody);
|
|
287
309
|
const bodyBuf = Buffer.from(JSON.stringify(reqBody), "utf8");
|
|
@@ -302,6 +324,7 @@ export async function responses(reqBody) {
|
|
|
302
324
|
method: "POST",
|
|
303
325
|
headers,
|
|
304
326
|
body: bodyBuf,
|
|
327
|
+
signal,
|
|
305
328
|
});
|
|
306
329
|
} catch (e) {
|
|
307
330
|
const cause = e?.cause;
|
|
@@ -310,7 +333,7 @@ export async function responses(reqBody) {
|
|
|
310
333
|
}
|
|
311
334
|
}
|
|
312
335
|
|
|
313
|
-
export async function responsesCompact(reqBody) {
|
|
336
|
+
export async function responsesCompact(reqBody, options = {}) {
|
|
314
337
|
// GitHub Copilot accepts Codex compact payloads on the regular Responses endpoint.
|
|
315
|
-
return responses(reqBody);
|
|
338
|
+
return responses(reqBody, options);
|
|
316
339
|
}
|