codex-copilot-dx 0.4.3 → 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 +209 -59
- 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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import * as zlib from "node:zlib";
|
|
3
5
|
import { chatCompletions, listModels, responses as copilotResponses, responsesCompact as copilotResponsesCompact } from "./copilot.mjs";
|
|
4
6
|
import { webStreamLines } from "./stream.mjs";
|
|
5
7
|
import { anthropicToChat, chatToAnthropic, streamAnthropicFromLines, countTokens } from "./anthropic.mjs";
|
|
@@ -19,6 +21,22 @@ const RESPONSES_ONLY = new Set([
|
|
|
19
21
|
|
|
20
22
|
const RESPONSE_HISTORY_LIMIT = 64;
|
|
21
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;
|
|
27
|
+
const gunzipAsync = promisify(zlib.gunzip);
|
|
28
|
+
const inflateAsync = promisify(zlib.inflate);
|
|
29
|
+
const brotliDecompressAsync = promisify(zlib.brotliDecompress);
|
|
30
|
+
const zstdDecompressAsync = zlib.zstdDecompress ? promisify(zlib.zstdDecompress) : null;
|
|
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);
|
|
22
40
|
|
|
23
41
|
export function requestPath(reqUrl) {
|
|
24
42
|
return new URL(reqUrl || "/", "http://localhost").pathname;
|
|
@@ -28,6 +46,127 @@ function cloneJson(value) {
|
|
|
28
46
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
29
47
|
}
|
|
30
48
|
|
|
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
|
+
|
|
65
|
+
const chunks = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
for await (const chunk of req) {
|
|
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);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat(chunks);
|
|
74
|
+
}
|
|
75
|
+
|
|
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) {
|
|
88
|
+
const encodings = String(contentEncoding || "identity")
|
|
89
|
+
.split(",")
|
|
90
|
+
.map((v) => v.trim().toLowerCase())
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
|
|
93
|
+
let decoded = buffer;
|
|
94
|
+
for (const encoding of encodings.reverse()) {
|
|
95
|
+
if (encoding === "identity") continue;
|
|
96
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
97
|
+
decoded = await decompressBody(gunzipAsync, decoded, maxBytes);
|
|
98
|
+
} else if (encoding === "deflate") {
|
|
99
|
+
decoded = await decompressBody(inflateAsync, decoded, maxBytes);
|
|
100
|
+
} else if (encoding === "br") {
|
|
101
|
+
decoded = await decompressBody(brotliDecompressAsync, decoded, maxBytes);
|
|
102
|
+
} else if (encoding === "zstd") {
|
|
103
|
+
if (!zstdDecompressAsync) throw new Error("Unsupported Content-Encoding: zstd");
|
|
104
|
+
decoded = await decompressBody(zstdDecompressAsync, decoded, maxBytes);
|
|
105
|
+
} else {
|
|
106
|
+
throw new Error(`Unsupported Content-Encoding: ${encoding}`);
|
|
107
|
+
}
|
|
108
|
+
if (decoded.length > maxBytes) throw payloadTooLarge("Decoded", maxBytes);
|
|
109
|
+
}
|
|
110
|
+
if (decoded.length > maxBytes) throw payloadTooLarge("Decoded", maxBytes);
|
|
111
|
+
return decoded;
|
|
112
|
+
}
|
|
113
|
+
|
|
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);
|
|
120
|
+
return JSON.parse(decoded.toString("utf8"));
|
|
121
|
+
}
|
|
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
|
+
|
|
31
170
|
function responsesInputItems(input) {
|
|
32
171
|
if (input === undefined || input === null) return [];
|
|
33
172
|
if (typeof input === "string") {
|
|
@@ -64,6 +203,10 @@ export function prepareResponsesRequest(reqBody) {
|
|
|
64
203
|
|
|
65
204
|
delete body.previous_response_id;
|
|
66
205
|
delete body.store;
|
|
206
|
+
if (Array.isArray(body.tools)) {
|
|
207
|
+
body.tools = body.tools.filter((tool) => tool?.type !== "image_generation");
|
|
208
|
+
if (!body.tools.length) delete body.tools;
|
|
209
|
+
}
|
|
67
210
|
|
|
68
211
|
return { body, inputItems: body.input };
|
|
69
212
|
}
|
|
@@ -112,8 +255,8 @@ function readSseEvents(buffer, onData) {
|
|
|
112
255
|
}
|
|
113
256
|
}
|
|
114
257
|
|
|
115
|
-
async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses) {
|
|
116
|
-
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 });
|
|
117
260
|
|
|
118
261
|
if (reqContext.body.stream) {
|
|
119
262
|
res.writeHead(resp.status, {
|
|
@@ -134,7 +277,7 @@ async function proxyCopilotResponses(reqContext, req, res, upstream = copilotRes
|
|
|
134
277
|
}
|
|
135
278
|
buffer += decoder.decode(value, { stream: true });
|
|
136
279
|
buffer = readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
|
|
137
|
-
res
|
|
280
|
+
await writeOrDrain(res, value);
|
|
138
281
|
}
|
|
139
282
|
} else {
|
|
140
283
|
const data = await resp.text();
|
|
@@ -211,11 +354,11 @@ function chatToResponses(chatResp, model) {
|
|
|
211
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 };
|
|
212
355
|
}
|
|
213
356
|
|
|
214
|
-
async function forwardToChat(chatReq, emitEvent, onDone, onError) {
|
|
357
|
+
async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
|
|
215
358
|
delete chatReq.max_tokens;
|
|
216
359
|
let resp;
|
|
217
360
|
try {
|
|
218
|
-
resp = await chatCompletions({ ...chatReq, stream: true });
|
|
361
|
+
resp = await chatCompletions({ ...chatReq, stream: true }, { signal: options.signal });
|
|
219
362
|
} catch (e) {
|
|
220
363
|
onError(502, e.message);
|
|
221
364
|
return;
|
|
@@ -227,37 +370,37 @@ async function forwardToChat(chatReq, emitEvent, onDone, onError) {
|
|
|
227
370
|
const respId = `resp_${uid()}`, itemId = `item_${uid()}`;
|
|
228
371
|
let actualModel = "unknown", fullText = "", toolCalls = {}, hasToolCalls = false;
|
|
229
372
|
|
|
230
|
-
emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
231
|
-
emitEvent("response.output_item.added", { output_index: 0, item: { type: "message", id: itemId, role: "assistant", status: "in_progress", content: [] } });
|
|
232
|
-
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: "" } });
|
|
233
376
|
|
|
234
|
-
const emitCompleted = () => {
|
|
377
|
+
const emitCompleted = async () => {
|
|
235
378
|
if (!hasToolCalls) {
|
|
236
|
-
emitEvent("response.output_text.done", { output_index: 0, content_index: 0, text: fullText });
|
|
237
|
-
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 }] } });
|
|
238
381
|
}
|
|
239
382
|
const output = hasToolCalls
|
|
240
383
|
? Object.entries(toolCalls).map(([id, tc]) => ({ type: "function_call", id, call_id: id, name: tc.name, arguments: tc.arguments, status: "completed" }))
|
|
241
384
|
: [{ type: "message", id: itemId, role: "assistant", status: "completed", content: [{ type: "output_text", text: fullText }] }];
|
|
242
|
-
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 } } });
|
|
243
386
|
};
|
|
244
387
|
|
|
245
388
|
try {
|
|
246
389
|
for await (const line of webStreamLines(resp)) {
|
|
247
390
|
if (!line.startsWith("data: ")) continue;
|
|
248
391
|
const data = line.slice(6).trim();
|
|
249
|
-
if (data === "[DONE]") { emitCompleted(); onDone(); return; }
|
|
392
|
+
if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
|
|
250
393
|
let parsed;
|
|
251
394
|
try { parsed = JSON.parse(data); } catch { continue; }
|
|
252
395
|
if (parsed.model) actualModel = parsed.model;
|
|
253
396
|
const delta = parsed.choices?.[0]?.delta;
|
|
254
397
|
if (!delta) continue;
|
|
255
|
-
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 }); }
|
|
256
399
|
if (delta.tool_calls) {
|
|
257
400
|
hasToolCalls = true;
|
|
258
401
|
for (const tc of delta.tool_calls) {
|
|
259
402
|
const id = tc.id || Object.keys(toolCalls)[tc.index] || `call_${tc.index}`;
|
|
260
|
-
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" } }); }
|
|
261
404
|
if (tc.function?.name) toolCalls[id].name += tc.function.name;
|
|
262
405
|
if (tc.function?.arguments) toolCalls[id].arguments += tc.function.arguments;
|
|
263
406
|
}
|
|
@@ -267,44 +410,44 @@ async function forwardToChat(chatReq, emitEvent, onDone, onError) {
|
|
|
267
410
|
onError(500, e?.message || "upstream stream error");
|
|
268
411
|
return;
|
|
269
412
|
}
|
|
270
|
-
emitCompleted();
|
|
413
|
+
await emitCompleted();
|
|
271
414
|
onDone();
|
|
272
415
|
}
|
|
273
416
|
|
|
274
417
|
// Public server entry point.
|
|
275
418
|
|
|
276
|
-
export function startAdapter(port = 2026) {
|
|
419
|
+
export function startAdapter(port = 2026, host = "127.0.0.1") {
|
|
277
420
|
const server = http.createServer((req, res) => {
|
|
278
421
|
const pathname = requestPath(req.url);
|
|
279
422
|
|
|
280
423
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
req.on("end", async () => {
|
|
424
|
+
(async () => {
|
|
425
|
+
const abort = createRequestAbort(req, res);
|
|
284
426
|
try {
|
|
285
|
-
const parsed =
|
|
427
|
+
const parsed = await readJsonBody(req);
|
|
428
|
+
if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
286
429
|
const prepared = prepareResponsesRequest(parsed);
|
|
287
430
|
prepared.surface = "responses";
|
|
288
431
|
const model = parsed.model || "unknown";
|
|
289
432
|
console.log(status("info", `responses model=${model} stream=${!!parsed.stream}`));
|
|
290
433
|
if (RESPONSES_ONLY.has(model)) {
|
|
291
|
-
await proxyCopilotResponses(prepared, req, res);
|
|
434
|
+
await proxyCopilotResponses(prepared, req, res, copilotResponses, { signal: abort.signal });
|
|
292
435
|
} else {
|
|
293
436
|
const chatReq = responsesToChat(prepared.body);
|
|
294
437
|
if (parsed.stream) {
|
|
295
|
-
forwardToChat(chatReq, (event, data) => {
|
|
438
|
+
await forwardToChat(chatReq, async (event, data) => {
|
|
296
439
|
if (!res.headersSent) res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
297
|
-
res
|
|
440
|
+
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
298
441
|
if (event === "response.completed") {
|
|
299
442
|
rememberResponseHistory(prepared, data.response);
|
|
300
443
|
recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
|
|
301
444
|
}
|
|
302
|
-
}, () => { 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 });
|
|
303
446
|
} else {
|
|
304
447
|
chatReq.stream = false;
|
|
305
448
|
delete chatReq.max_tokens;
|
|
306
449
|
try {
|
|
307
|
-
const upstream = await chatCompletions({ ...chatReq, stream: false });
|
|
450
|
+
const upstream = await chatCompletions({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
308
451
|
const data = await upstream.text();
|
|
309
452
|
const resp = chatToResponses(JSON.parse(data), model);
|
|
310
453
|
rememberResponseHistory(prepared, resp);
|
|
@@ -319,67 +462,72 @@ export function startAdapter(port = 2026) {
|
|
|
319
462
|
}
|
|
320
463
|
} catch (e) {
|
|
321
464
|
console.error(status("err", `Responses request failed: ${e.message}`));
|
|
322
|
-
|
|
323
|
-
|
|
465
|
+
sendJsonError(res, e);
|
|
466
|
+
} finally {
|
|
467
|
+
abort.cleanup();
|
|
324
468
|
}
|
|
325
|
-
});
|
|
469
|
+
})();
|
|
326
470
|
return;
|
|
327
471
|
}
|
|
328
472
|
|
|
329
473
|
if (req.method === "POST" && pathname === "/v1/responses/compact") {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
req.on("end", async () => {
|
|
474
|
+
(async () => {
|
|
475
|
+
const abort = createRequestAbort(req, res);
|
|
333
476
|
try {
|
|
334
|
-
const parsed =
|
|
477
|
+
const parsed = await readJsonBody(req);
|
|
478
|
+
if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
335
479
|
const prepared = prepareResponsesRequest(parsed);
|
|
336
480
|
prepared.surface = "responses_compact";
|
|
337
481
|
const model = parsed.model || "unknown";
|
|
338
482
|
console.log(status("info", `responses compact model=${model} stream=${!!parsed.stream}`));
|
|
339
|
-
await proxyCopilotResponses(prepared, req, res, copilotResponsesCompact);
|
|
483
|
+
await proxyCopilotResponses(prepared, req, res, copilotResponsesCompact, { signal: abort.signal });
|
|
340
484
|
} catch (e) {
|
|
341
485
|
console.error(status("err", `Responses compact request failed: ${e.message}`));
|
|
342
|
-
|
|
343
|
-
|
|
486
|
+
sendJsonError(res, e);
|
|
487
|
+
} finally {
|
|
488
|
+
abort.cleanup();
|
|
344
489
|
}
|
|
345
|
-
});
|
|
490
|
+
})();
|
|
346
491
|
return;
|
|
347
492
|
}
|
|
348
493
|
|
|
349
494
|
if (req.method === "GET" && pathname === "/v1/models") {
|
|
350
|
-
|
|
495
|
+
const abort = createRequestAbort(req, res);
|
|
496
|
+
abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
497
|
+
listModels({ signal: abort.signal })
|
|
351
498
|
.then(({ status, body }) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(body); })
|
|
352
|
-
.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());
|
|
353
501
|
return;
|
|
354
502
|
}
|
|
355
503
|
|
|
356
504
|
if (req.method === "POST" && pathname === "/v1/messages/count_tokens") {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
req.on("end", () => {
|
|
505
|
+
(async () => {
|
|
506
|
+
const abort = createRequestAbort(req, res);
|
|
360
507
|
try {
|
|
361
|
-
const parsed =
|
|
508
|
+
const parsed = await readJsonBody(req);
|
|
362
509
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
363
510
|
res.end(JSON.stringify(countTokens(parsed)));
|
|
364
511
|
} catch (e) {
|
|
365
|
-
|
|
366
|
-
|
|
512
|
+
sendJsonError(res, e);
|
|
513
|
+
} finally {
|
|
514
|
+
abort.cleanup();
|
|
367
515
|
}
|
|
368
|
-
});
|
|
516
|
+
})();
|
|
369
517
|
return;
|
|
370
518
|
}
|
|
371
519
|
|
|
372
520
|
if (req.method === "POST" && pathname === "/v1/messages") {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
req.on("end", async () => {
|
|
521
|
+
(async () => {
|
|
522
|
+
const abort = createRequestAbort(req, res);
|
|
376
523
|
try {
|
|
377
|
-
const parsed =
|
|
524
|
+
const parsed = await readJsonBody(req);
|
|
525
|
+
if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
|
|
378
526
|
const model = parsed.model || "unknown";
|
|
379
527
|
console.log(status("info", `messages model=${model} stream=${!!parsed.stream}`));
|
|
380
528
|
const chatReq = anthropicToChat(parsed);
|
|
381
529
|
if (parsed.stream) {
|
|
382
|
-
const upstream = await chatCompletions({ ...chatReq, stream: true });
|
|
530
|
+
const upstream = await chatCompletions({ ...chatReq, stream: true }, { signal: abort.signal });
|
|
383
531
|
if (!upstream.ok) {
|
|
384
532
|
if (!res.headersSent) res.writeHead(upstream.status);
|
|
385
533
|
res.end(await upstream.text());
|
|
@@ -390,17 +538,17 @@ export function startAdapter(port = 2026) {
|
|
|
390
538
|
let usage;
|
|
391
539
|
await streamAnthropicFromLines(
|
|
392
540
|
webStreamLines(upstream),
|
|
393
|
-
(event, data) => {
|
|
541
|
+
async (event, data) => {
|
|
394
542
|
if (event === "message_start") messageId = data.message?.id;
|
|
395
543
|
if (event === "message_delta") usage = data.usage;
|
|
396
|
-
res
|
|
544
|
+
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
397
545
|
},
|
|
398
546
|
model,
|
|
399
547
|
);
|
|
400
548
|
recordAnthropicUsage({ surface: "messages", mode: "stream", model, responseId: messageId, usage });
|
|
401
549
|
if (!res.writableEnded) res.end();
|
|
402
550
|
} else {
|
|
403
|
-
const upstream = await chatCompletions({ ...chatReq, stream: false });
|
|
551
|
+
const upstream = await chatCompletions({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
404
552
|
const data = await upstream.text();
|
|
405
553
|
const anthropicMsg = chatToAnthropic(JSON.parse(data), model);
|
|
406
554
|
recordAnthropicUsage({ surface: "messages", mode: "json", model: anthropicMsg.model, responseId: anthropicMsg.id, usage: anthropicMsg.usage });
|
|
@@ -409,10 +557,11 @@ export function startAdapter(port = 2026) {
|
|
|
409
557
|
}
|
|
410
558
|
} catch (e) {
|
|
411
559
|
console.error(status("err", `Messages request failed: ${e.message}`));
|
|
412
|
-
|
|
413
|
-
|
|
560
|
+
sendJsonError(res, e);
|
|
561
|
+
} finally {
|
|
562
|
+
abort.cleanup();
|
|
414
563
|
}
|
|
415
|
-
});
|
|
564
|
+
})();
|
|
416
565
|
return;
|
|
417
566
|
}
|
|
418
567
|
|
|
@@ -431,8 +580,9 @@ export function startAdapter(port = 2026) {
|
|
|
431
580
|
});
|
|
432
581
|
|
|
433
582
|
return new Promise((resolve) => {
|
|
434
|
-
server.listen(port, () => {
|
|
435
|
-
|
|
583
|
+
server.listen(port, host, () => {
|
|
584
|
+
const actualPort = server.address()?.port || port;
|
|
585
|
+
console.log(status("ok", `Adapter listening on http://${host}:${actualPort}`));
|
|
436
586
|
resolve(server);
|
|
437
587
|
});
|
|
438
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
|
}
|