codex-copilot-dx 0.1.6 → 0.4.1

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.
@@ -0,0 +1,316 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { randomUUID } from "node:crypto";
5
+ import { status } from "./status.mjs";
6
+
7
+ const GITHUB_TOKEN_PATH = path.join(os.homedir(), ".local", "share", "copilot-api", "github_token");
8
+ export const DEFAULT_API_BASE = "https://api.githubcopilot.com";
9
+ let apiBase = DEFAULT_API_BASE;
10
+ const IMG_MAX_DIM = parseInt(process.env.CCDX_IMG_MAX_DIM || "2048", 10);
11
+ const IMG_QUALITY = parseInt(process.env.CCDX_IMG_QUALITY || "85", 10);
12
+ const IMG_MIN_BYTES = parseInt(process.env.CCDX_IMG_MIN_BYTES || "100000", 10);
13
+ const IMG_OPT_DISABLED = process.env.CCDX_DISABLE_IMG_OPT === "1";
14
+ let sharpImport = null;
15
+
16
+ export function parseApiBase(data) {
17
+ return (data && data.endpoints && typeof data.endpoints.api === "string" && data.endpoints.api)
18
+ ? data.endpoints.api
19
+ : DEFAULT_API_BASE;
20
+ }
21
+
22
+ export function getApiBase() {
23
+ return apiBase;
24
+ }
25
+
26
+ export function responsesEndpointPath() {
27
+ return "/responses";
28
+ }
29
+ const GITHUB_API = "https://api.github.com";
30
+
31
+ async function sharp() {
32
+ sharpImport ||= import("sharp");
33
+ const mod = await sharpImport;
34
+ return mod.default || mod;
35
+ }
36
+
37
+ export async function optimizeImageDataUrl(dataUrl) {
38
+ if (IMG_OPT_DISABLED) return dataUrl;
39
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:")) return dataUrl;
40
+ const match = /^data:(image\/[a-z+.-]+);base64,(.+)$/i.exec(dataUrl);
41
+ if (!match) return dataUrl;
42
+ const mime = match[1].toLowerCase();
43
+ const raw = Buffer.from(match[2], "base64");
44
+ if (raw.length < IMG_MIN_BYTES || mime.includes("gif")) return dataUrl;
45
+
46
+ try {
47
+ const resize = await sharp();
48
+ const out = await resize(raw, { failOn: "none" })
49
+ .rotate()
50
+ .resize(IMG_MAX_DIM, IMG_MAX_DIM, { fit: "inside", withoutEnlargement: true })
51
+ .webp({ quality: IMG_QUALITY, effort: 4 })
52
+ .toBuffer();
53
+ const ratio = ((out.length / raw.length) * 100).toFixed(1);
54
+ console.log(status("info", `image ${(raw.length / 1024).toFixed(0)}KB ${mime} -> ${(out.length / 1024).toFixed(0)}KB webp (${ratio}%)`));
55
+ return `data:image/webp;base64,${out.toString("base64")}`;
56
+ } catch (e) {
57
+ console.warn(status("warn", `image optimize failed (${mime}, ${raw.length}b): ${e.message}`));
58
+ return dataUrl;
59
+ }
60
+ }
61
+
62
+ function visitImageParts(parts, tasks) {
63
+ if (!Array.isArray(parts)) return;
64
+ for (const part of parts) {
65
+ if (!part) continue;
66
+ if (part.type === "input_image" && typeof part.image_url === "string") {
67
+ tasks.push((async () => { part.image_url = await optimizeImageDataUrl(part.image_url); })());
68
+ } else if (part.type === "image" && part.source?.type === "base64" && part.source?.data) {
69
+ tasks.push((async () => {
70
+ const dataUrl = `data:${part.source.media_type || "image/png"};base64,${part.source.data}`;
71
+ const opt = await optimizeImageDataUrl(dataUrl);
72
+ const match = /^data:([^;]+);base64,(.+)$/.exec(opt);
73
+ if (match) {
74
+ part.source.media_type = match[1];
75
+ part.source.data = match[2];
76
+ }
77
+ })());
78
+ }
79
+ }
80
+ }
81
+
82
+ export async function optimizeImagesInBody(reqBody) {
83
+ if (IMG_OPT_DISABLED || !Array.isArray(reqBody.input)) return reqBody;
84
+ const tasks = [];
85
+
86
+ for (const item of reqBody.input) {
87
+ if (!item) continue;
88
+ if (item.type === "message" && Array.isArray(item.content)) {
89
+ visitImageParts(item.content, tasks);
90
+ }
91
+ if (item.type === "function_call_output") {
92
+ if (Array.isArray(item.output)) {
93
+ visitImageParts(item.output, tasks);
94
+ } else if (typeof item.output === "string") {
95
+ const trimmed = item.output.trim();
96
+ if (trimmed.startsWith("[")) {
97
+ try {
98
+ const parsed = JSON.parse(trimmed);
99
+ if (Array.isArray(parsed)) {
100
+ const before = tasks.length;
101
+ visitImageParts(parsed, tasks);
102
+ const localTasks = tasks.slice(before);
103
+ if (localTasks.length) {
104
+ tasks.push(Promise.all(localTasks).then(() => {
105
+ item.output = JSON.stringify(parsed);
106
+ }));
107
+ }
108
+ }
109
+ } catch {
110
+ // Leave non-JSON tool output untouched.
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ if (tasks.length) await Promise.all(tasks);
118
+ return reqBody;
119
+ }
120
+
121
+ export function summarizeReqBody(reqBody) {
122
+ try {
123
+ const input = reqBody.input;
124
+ if (!Array.isArray(input)) return { items: 0, images: 0, biggest: 0, biggestKind: "n/a" };
125
+ let biggest = 0;
126
+ let biggestKind = "";
127
+ let images = 0;
128
+ const countImages = (parts) => {
129
+ if (!Array.isArray(parts)) return;
130
+ for (const part of parts) {
131
+ if (part?.type === "input_image" || part?.type === "image") images++;
132
+ }
133
+ };
134
+
135
+ for (const item of input) {
136
+ const size = JSON.stringify(item).length;
137
+ if (size > biggest) {
138
+ biggest = size;
139
+ biggestKind = item?.type || "?";
140
+ }
141
+ if (item?.type === "message") countImages(item.content);
142
+ if (item?.type === "function_call_output") {
143
+ if (Array.isArray(item.output)) {
144
+ countImages(item.output);
145
+ } else if (typeof item.output === "string" && item.output.trim().startsWith("[")) {
146
+ try {
147
+ const parsed = JSON.parse(item.output);
148
+ if (Array.isArray(parsed)) countImages(parsed);
149
+ } catch {
150
+ // Ignore non-JSON tool output.
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ return { items: input.length, images, biggest, biggestKind };
157
+ } catch {
158
+ return { items: -1, images: -1, biggest: -1, biggestKind: "err" };
159
+ }
160
+ }
161
+
162
+ export function computeInitiator(messages) {
163
+ const isAgent = Array.isArray(messages)
164
+ && messages.some((m) => m && ["assistant", "tool"].includes(m.role));
165
+ return isAgent ? "agent" : "user";
166
+ }
167
+
168
+ export function computeVision(messages) {
169
+ return Array.isArray(messages) && messages.some(
170
+ (m) => m && typeof m.content !== "string"
171
+ && Array.isArray(m.content)
172
+ && m.content.some((p) => p && p.type === "image_url"),
173
+ );
174
+ }
175
+
176
+ export function buildHeaders({ token, version, initiator, vision }) {
177
+ const h = {
178
+ "Authorization": `Bearer ${token}`,
179
+ "Content-Type": "application/json",
180
+ "Copilot-Integration-Id": "vscode-chat",
181
+ "Editor-Version": `vscode/${version}`,
182
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
183
+ "User-Agent": "GitHubCopilotChat/0.26.7",
184
+ "Openai-Intent": "conversation-panel",
185
+ "X-Github-Api-Version": "2025-04-01",
186
+ "X-Request-Id": randomUUID(),
187
+ "X-Vscode-User-Agent-Library-Version": "electron-fetch",
188
+ "X-Initiator": initiator,
189
+ };
190
+ if (vision) h["Copilot-Vision-Request"] = "true";
191
+ return h;
192
+ }
193
+
194
+ export const FALLBACK_VSCODE_VERSION = "1.122.1";
195
+
196
+ let cachedVersion = FALLBACK_VSCODE_VERSION;
197
+
198
+ export function parseVSCodeVersion(json) {
199
+ return (json && typeof json.productVersion === "string" && json.productVersion)
200
+ ? json.productVersion
201
+ : FALLBACK_VSCODE_VERSION;
202
+ }
203
+
204
+ export function getVSCodeVersion() {
205
+ return cachedVersion;
206
+ }
207
+
208
+ // Refresh the VS Code version asynchronously; keep the fallback on failure.
209
+ export async function refreshVSCodeVersion() {
210
+ try {
211
+ const ctrl = new AbortController();
212
+ const timer = setTimeout(() => ctrl.abort(), 5000);
213
+ const resp = await fetch(
214
+ "https://update.code.visualstudio.com/api/update/darwin-arm64/stable/latest",
215
+ { signal: ctrl.signal },
216
+ );
217
+ clearTimeout(timer);
218
+ if (resp.ok) {
219
+ cachedVersion = parseVSCodeVersion(await resp.json());
220
+ console.log(status("info", `VS Code version: ${cachedVersion}`));
221
+ }
222
+ } catch {
223
+ // Keep the fallback quietly.
224
+ }
225
+ return cachedVersion;
226
+ }
227
+
228
+ let copilotToken = null;
229
+ let copilotTokenExpiry = 0;
230
+
231
+ function getGithubToken() {
232
+ if (!fs.existsSync(GITHUB_TOKEN_PATH)) {
233
+ throw new Error("GitHub token not found. Run the tool once to log in.");
234
+ }
235
+ return fs.readFileSync(GITHUB_TOKEN_PATH, "utf-8").trim();
236
+ }
237
+
238
+ export async function getCopilotToken() {
239
+ if (copilotToken && Date.now() < copilotTokenExpiry - 60000) return copilotToken;
240
+ const ghToken = getGithubToken();
241
+ const resp = await fetch(`${GITHUB_API}/copilot_internal/v2/token`, {
242
+ headers: { Authorization: `token ${ghToken}`, Accept: "application/json" },
243
+ });
244
+ if (!resp.ok) throw new Error(`Failed to get Copilot token: ${resp.status}`);
245
+ const data = await resp.json();
246
+ if (!data.token) throw new Error("Copilot token response missing token field");
247
+ copilotToken = data.token;
248
+ apiBase = parseApiBase(data);
249
+ copilotTokenExpiry = typeof data.expires_at === "number"
250
+ ? data.expires_at * 1000
251
+ : Date.now() + 25 * 60 * 1000; // fallback if expires_at absent: refresh in ~25min
252
+ console.log(status("ok", "Copilot token refreshed"));
253
+ return copilotToken;
254
+ }
255
+
256
+ // chatReq is already converted by the adapter. The caller parses the raw Response.
257
+ export async function chatCompletions(chatReq) {
258
+ const token = await getCopilotToken();
259
+ const messages = chatReq.messages || [];
260
+ const headers = buildHeaders({
261
+ token,
262
+ version: getVSCodeVersion(),
263
+ initiator: computeInitiator(messages),
264
+ vision: computeVision(messages),
265
+ });
266
+ return fetch(`${getApiBase()}/chat/completions`, {
267
+ method: "POST",
268
+ headers,
269
+ body: JSON.stringify(chatReq),
270
+ });
271
+ }
272
+
273
+ export async function listModels() {
274
+ const token = await getCopilotToken();
275
+ const headers = buildHeaders({
276
+ token, version: getVSCodeVersion(), initiator: "user", vision: false,
277
+ });
278
+ const resp = await fetch(`${getApiBase()}/models`, { headers });
279
+ return { status: resp.status, body: await resp.text() };
280
+ }
281
+
282
+ // Responses-only models go directly to Copilot's /responses endpoint.
283
+ export async function responses(reqBody) {
284
+ const token = await getCopilotToken();
285
+ const beforeBytes = Buffer.byteLength(JSON.stringify(reqBody));
286
+ await optimizeImagesInBody(reqBody);
287
+ const bodyBuf = Buffer.from(JSON.stringify(reqBody), "utf8");
288
+ const summary = summarizeReqBody(reqBody);
289
+ console.log(status("info", `responses payload bytes=${bodyBuf.length} was=${beforeBytes} input_items=${summary.items} images=${summary.images} biggest_item=${summary.biggest}b (${summary.biggestKind})`));
290
+ const headers = buildHeaders({
291
+ token,
292
+ version: getVSCodeVersion(),
293
+ initiator: "user",
294
+ vision: false,
295
+ });
296
+ headers["Content-Type"] = "application/json; charset=utf-8";
297
+ headers["Content-Length"] = String(bodyBuf.length);
298
+ headers["Accept"] = reqBody.stream ? "text/event-stream" : "application/json";
299
+
300
+ try {
301
+ return await fetch(`${getApiBase()}${responsesEndpointPath()}`, {
302
+ method: "POST",
303
+ headers,
304
+ body: bodyBuf,
305
+ });
306
+ } catch (e) {
307
+ const cause = e?.cause;
308
+ const causeText = cause ? ` (${[cause.code, cause.message].filter(Boolean).join(": ")})` : "";
309
+ throw new Error(`Copilot responses fetch failed: ${e.message}${causeText}`);
310
+ }
311
+ }
312
+
313
+ export async function responsesCompact(reqBody) {
314
+ // GitHub Copilot accepts Codex compact payloads on the regular Responses endpoint.
315
+ return responses(reqBody);
316
+ }
package/src/launcher.mjs CHANGED
@@ -1,90 +1,19 @@
1
- import { spawn, execSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import os from "node:os";
5
- import net from "node:net";
1
+ import { spawn } from "node:child_process";
2
+ import { status } from "./status.mjs";
6
3
 
7
- const GITHUB_TOKEN_PATH = path.join(os.homedir(), ".local", "share", "copilot-api", "github_token");
8
-
9
- function isPortAccepting(port) {
10
- return new Promise((resolve) => {
11
- const sock = net.connect(port, "127.0.0.1");
12
- let done = false;
13
- const finish = (v) => { if (done) return; done = true; sock.destroy(); resolve(v); };
14
- sock.once("connect", () => finish(true));
15
- sock.once("error", () => finish(false));
16
- sock.setTimeout(1000, () => finish(false));
17
- });
18
- }
19
-
20
- export async function ensureAuth() {
21
- if (fs.existsSync(GITHUB_TOKEN_PATH)) {
22
- console.log("[codex-copilot-dx] GitHub token found");
23
- return;
24
- }
25
- console.log("[codex-copilot-dx] No GitHub token. Running auth...");
26
- const copilotApiBin = findCopilotApi();
27
- execSync(`${copilotApiBin} auth`, { stdio: "inherit" });
28
- }
29
-
30
- function findCopilotApi() {
31
- try {
32
- return execSync("which copilot-api", { encoding: "utf-8" }).trim();
33
- } catch {
34
- // Try npx path
35
- const npxPath = path.join(os.homedir(), ".npm/_npx");
36
- if (fs.existsSync(npxPath)) {
37
- for (const dir of fs.readdirSync(npxPath)) {
38
- const bin = path.join(npxPath, dir, "node_modules/.bin/copilot-api");
39
- if (fs.existsSync(bin)) return bin;
40
- }
41
- }
42
- return "npx copilot-api";
43
- }
44
- }
45
-
46
- export async function startCopilotApi(port = 4141) {
47
- if (await isPortAccepting(port)) {
48
- console.log(`[codex-copilot-dx] copilot-api already running on :${port}`);
49
- return null;
50
- }
51
- console.log(`[codex-copilot-dx] Starting copilot-api on :${port}...`);
52
- const copilotApiBin = findCopilotApi();
53
- const child = spawn(copilotApiBin, ["start", "--port", String(port)], {
54
- stdio: "pipe",
55
- detached: false,
56
- });
57
- child.stdout?.on("data", (d) => {
58
- const line = d.toString().trim();
59
- if (line) console.log(`[copilot-api] ${line}`);
60
- });
61
- child.stderr?.on("data", (d) => {
62
- const line = d.toString().trim();
63
- if (line) console.log(`[copilot-api] ${line}`);
64
- });
65
-
66
- // Wait for it to be ready
67
- for (let i = 0; i < 30; i++) {
68
- await new Promise((r) => setTimeout(r, 500));
69
- if (await isPortAccepting(port)) {
70
- console.log(`[codex-copilot-dx] copilot-api ready on :${port}`);
71
- return child;
72
- }
73
- }
74
- throw new Error("copilot-api failed to start");
75
- }
4
+ export { ensureAuth } from "./auth.mjs";
76
5
 
77
6
  export function openCodex() {
78
7
  if (process.platform === "darwin") {
79
8
  try {
80
9
  spawn("open", ["/Applications/Codex.app"], { detached: true, stdio: "ignore" });
81
- console.log("[codex-copilot-dx] Codex app launched");
10
+ console.log(status("ok", "Codex app launched"));
82
11
  } catch {
83
- console.log("[codex-copilot-dx] Codex app not found at /Applications/Codex.app");
84
- console.log(" Download from: https://openai.com/codex");
12
+ console.log(status("warn", "Codex app not found at /Applications/Codex.app"));
13
+ console.log(status("info", "Download Codex from https://openai.com/codex"));
85
14
  }
86
15
  } else {
87
- console.log("[codex-copilot-dx] Auto-launch not supported on this platform.");
88
- console.log(" Open Codex manually, it will connect to the adapter.");
16
+ console.log(status("warn", "Auto-launch is not supported on this platform"));
17
+ console.log(status("info", "Open Codex manually; it will connect to the adapter."));
89
18
  }
90
19
  }
package/src/status.mjs ADDED
@@ -0,0 +1,11 @@
1
+ const LABELS = {
2
+ info: "[INFO]",
3
+ ok: "[OK]",
4
+ wait: "[WAIT]",
5
+ warn: "[WARN]",
6
+ err: "[ERR]",
7
+ };
8
+
9
+ export function status(kind, message) {
10
+ return `${LABELS[kind] || LABELS.info} ${message}`;
11
+ }
package/src/stream.mjs ADDED
@@ -0,0 +1,22 @@
1
+ // Yield fetch Response body lines while preserving SSE semantics for callers.
2
+ // Strip CRLF tails and release the reader if iteration stops early.
3
+ export async function* webStreamLines(response) {
4
+ const reader = response.body.getReader();
5
+ const decoder = new TextDecoder();
6
+ let buf = "";
7
+ try {
8
+ while (true) {
9
+ const { done, value } = await reader.read();
10
+ if (done) break;
11
+ buf += decoder.decode(value, { stream: true });
12
+ const lines = buf.split("\n");
13
+ buf = lines.pop();
14
+ for (const line of lines) yield line.replace(/\r$/, "");
15
+ }
16
+ buf += decoder.decode();
17
+ if (buf.length > 0) yield buf.replace(/\r$/, "");
18
+ } finally {
19
+ await reader.cancel().catch(() => {});
20
+ reader.releaseLock();
21
+ }
22
+ }
package/src/usage.mjs ADDED
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const DEFAULT_USAGE_PATH = path.join(os.homedir(), ".local", "share", "codex-copilot-dx", "usage.jsonl");
6
+
7
+ let writeQueue = Promise.resolve();
8
+
9
+ export function usageLogPath() {
10
+ return process.env.CCDX_USAGE_PATH || DEFAULT_USAGE_PATH;
11
+ }
12
+
13
+ function numberOrUndefined(value) {
14
+ return Number.isFinite(value) ? value : undefined;
15
+ }
16
+
17
+ function positiveNumber(value) {
18
+ const n = numberOrUndefined(value);
19
+ return n && n > 0 ? n : undefined;
20
+ }
21
+
22
+ function compactObject(obj) {
23
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null));
24
+ }
25
+
26
+ function hasPositiveTokenValue(usage) {
27
+ return Object.entries(usage || {}).some(([k, v]) => k.endsWith("_tokens") && Number.isFinite(v) && v > 0);
28
+ }
29
+
30
+ function normalizeResponsesUsage(usage) {
31
+ if (!usage || typeof usage !== "object") return undefined;
32
+ const out = compactObject({
33
+ input_tokens: positiveNumber(usage.input_tokens),
34
+ cached_input_tokens: positiveNumber(usage.cached_input_tokens ?? usage.input_tokens_details?.cached_tokens),
35
+ output_tokens: positiveNumber(usage.output_tokens),
36
+ reasoning_output_tokens: positiveNumber(usage.reasoning_output_tokens ?? usage.output_tokens_details?.reasoning_tokens),
37
+ total_tokens: positiveNumber(usage.total_tokens),
38
+ });
39
+ if (out.total_tokens === undefined && (out.input_tokens !== undefined || out.output_tokens !== undefined)) {
40
+ out.total_tokens = (out.input_tokens || 0) + (out.output_tokens || 0);
41
+ }
42
+ return hasPositiveTokenValue(out) ? out : undefined;
43
+ }
44
+
45
+ function normalizeAnthropicUsage(usage) {
46
+ if (!usage || typeof usage !== "object") return undefined;
47
+ const out = compactObject({
48
+ input_tokens: positiveNumber(usage.input_tokens),
49
+ cache_read_input_tokens: positiveNumber(usage.cache_read_input_tokens),
50
+ cache_creation_input_tokens: positiveNumber(usage.cache_creation_input_tokens),
51
+ output_tokens: positiveNumber(usage.output_tokens),
52
+ });
53
+ if (out.input_tokens !== undefined || out.cache_read_input_tokens !== undefined || out.cache_creation_input_tokens !== undefined || out.output_tokens !== undefined) {
54
+ out.total_tokens = (out.input_tokens || 0) + (out.cache_read_input_tokens || 0) + (out.cache_creation_input_tokens || 0) + (out.output_tokens || 0);
55
+ }
56
+ return hasPositiveTokenValue(out) ? out : undefined;
57
+ }
58
+
59
+ function normalizeCopilotUsage(copilotUsage) {
60
+ if (!copilotUsage || typeof copilotUsage !== "object") return undefined;
61
+ const out = {};
62
+ for (const detail of copilotUsage.token_details || []) {
63
+ const tokens = positiveNumber(detail?.token_count);
64
+ if (!tokens) continue;
65
+ switch (detail.token_type) {
66
+ case "input":
67
+ out.input_tokens = (out.input_tokens || 0) + tokens;
68
+ break;
69
+ case "cache_read":
70
+ out.cache_read_tokens = (out.cache_read_tokens || 0) + tokens;
71
+ break;
72
+ case "output":
73
+ out.output_tokens = (out.output_tokens || 0) + tokens;
74
+ break;
75
+ default:
76
+ out[`${detail.token_type || "unknown"}_tokens`] = (out[`${detail.token_type || "unknown"}_tokens`] || 0) + tokens;
77
+ break;
78
+ }
79
+ }
80
+ out.total_tokens = (out.input_tokens || 0) + (out.cache_read_tokens || 0) + (out.output_tokens || 0);
81
+ if (Number.isFinite(copilotUsage.total_nano_aiu)) out.total_nano_aiu = copilotUsage.total_nano_aiu;
82
+ return hasPositiveTokenValue(out) ? out : undefined;
83
+ }
84
+
85
+ export function buildResponsesUsageRecord({ surface = "responses", mode, model, response, event } = {}) {
86
+ const responseObj = response || event?.response;
87
+ const usage = normalizeResponsesUsage(responseObj?.usage);
88
+ const copilotUsage = normalizeCopilotUsage(event?.copilot_usage || responseObj?.copilot_usage);
89
+ if (!usage && !copilotUsage) return null;
90
+ return compactObject({
91
+ ts: new Date().toISOString(),
92
+ surface,
93
+ mode,
94
+ model: responseObj?.model || model,
95
+ response_id: responseObj?.id,
96
+ usage,
97
+ copilot_usage: copilotUsage,
98
+ });
99
+ }
100
+
101
+ export function buildAnthropicUsageRecord({ surface = "messages", mode, model, responseId, usage } = {}) {
102
+ const normalized = normalizeAnthropicUsage(usage);
103
+ if (!normalized) return null;
104
+ return compactObject({
105
+ ts: new Date().toISOString(),
106
+ surface,
107
+ mode,
108
+ model,
109
+ response_id: responseId,
110
+ usage: normalized,
111
+ });
112
+ }
113
+
114
+ export function recordUsage(record) {
115
+ if (!record || process.env.CCDX_DISABLE_USAGE === "1") return Promise.resolve();
116
+ const filePath = usageLogPath();
117
+ const line = `${JSON.stringify(record)}\n`;
118
+ writeQueue = writeQueue
119
+ .catch(() => {})
120
+ .then(async () => {
121
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
122
+ await fs.promises.appendFile(filePath, line, { encoding: "utf8", mode: 0o600 });
123
+ })
124
+ .catch((e) => console.error(`codex-copilot-dx usage log write failed: ${e.message}`));
125
+ return writeQueue;
126
+ }
127
+
128
+ export function recordResponsesUsage(args) {
129
+ return recordUsage(buildResponsesUsageRecord(args));
130
+ }
131
+
132
+ export function recordAnthropicUsage(args) {
133
+ return recordUsage(buildAnthropicUsageRecord(args));
134
+ }
135
+
136
+ export async function flushUsageWritesForTests() {
137
+ await writeQueue;
138
+ }
139
+
140
+ export async function readUsageRecords(filePath = usageLogPath()) {
141
+ try {
142
+ const text = await fs.promises.readFile(filePath, "utf8");
143
+ return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
144
+ } catch (e) {
145
+ if (e.code === "ENOENT") return [];
146
+ throw e;
147
+ }
148
+ }
149
+
150
+ function addUsageTotals(target, usage = {}) {
151
+ for (const [key, value] of Object.entries(usage)) {
152
+ if (Number.isFinite(value)) target[key] = (target[key] || 0) + value;
153
+ }
154
+ }
155
+
156
+ export function summarizeUsage(records) {
157
+ const totals = {};
158
+ const byModel = new Map();
159
+ for (const record of records) {
160
+ addUsageTotals(totals, record.usage);
161
+ if (record.copilot_usage) {
162
+ totals.copilot_total_tokens = (totals.copilot_total_tokens || 0) + (record.copilot_usage.total_tokens || 0);
163
+ totals.total_nano_aiu = (totals.total_nano_aiu || 0) + (record.copilot_usage.total_nano_aiu || 0);
164
+ }
165
+ const model = record.model || "unknown";
166
+ if (!byModel.has(model)) byModel.set(model, { requests: 0 });
167
+ const modelTotals = byModel.get(model);
168
+ modelTotals.requests += 1;
169
+ addUsageTotals(modelTotals, record.usage);
170
+ }
171
+ return { requests: records.length, totals, byModel: Object.fromEntries(byModel) };
172
+ }
173
+
174
+ function fmt(n) {
175
+ return Number.isFinite(n) ? n.toLocaleString("en-US") : "0";
176
+ }
177
+
178
+ export async function printUsageSummary() {
179
+ const records = await readUsageRecords();
180
+ const summary = summarizeUsage(records);
181
+ console.log(`Usage log: ${usageLogPath()}`);
182
+ if (summary.requests === 0) {
183
+ console.log("No usage records yet.");
184
+ return;
185
+ }
186
+ console.log(`Requests: ${fmt(summary.requests)}`);
187
+ console.log(`Tokens: input=${fmt(summary.totals.input_tokens)} cache_read=${fmt(summary.totals.cache_read_input_tokens || summary.totals.cached_input_tokens)} output=${fmt(summary.totals.output_tokens)} total=${fmt(summary.totals.total_tokens)}`);
188
+ console.log("\nBy model:");
189
+ for (const [model, row] of Object.entries(summary.byModel)) {
190
+ console.log(` ${model}: requests=${fmt(row.requests)} input=${fmt(row.input_tokens)} cache_read=${fmt(row.cache_read_input_tokens || row.cached_input_tokens)} output=${fmt(row.output_tokens)} total=${fmt(row.total_tokens)}`);
191
+ }
192
+ }
package/CHANGELOG.md DELETED
@@ -1,19 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.1.6
4
-
5
- ### Fixed
6
- - **413 Payload Too Large** on `/v1/responses` for long computer-use sessions. The Codex Desktop computer-use tool embeds screenshots as `input_image` parts inside `function_call_output.output` (sometimes as a stringified JSON array). After dozens of tool turns these accumulate to >5MB and hit GitHub Copilot's request size limit. The adapter now walks tool outputs, decodes embedded base64 images, and re-encodes them as WebP at long-edge ≤ 2048px before forwarding to the upstream.
7
-
8
- ### Added
9
- - Automatic image optimization with `sharp`. Tunable via env vars:
10
- - `CCDX_IMG_MAX_DIM` (default `2048`) – max long edge in pixels
11
- - `CCDX_IMG_QUALITY` (default `85`) – WebP quality
12
- - `CCDX_IMG_MIN_BYTES` (default `100000`) – skip images smaller than this
13
- - `CCDX_DISABLE_IMG_OPT=1` – disable optimization entirely
14
- - `CCDX_DEBUG=1` – verbose diagnostics
15
- - Per-request log line with byte counts before/after optimization, image count, and biggest item size.
16
- - Upstream error responses (status, headers, body) are now logged when non-2xx, making 413/auth issues much easier to diagnose.
17
-
18
- ### Internal
19
- - Request headers to Copilot now set explicit `Content-Length`, `Content-Type: application/json; charset=utf-8`, and `Accept` to avoid undici picking chunked transfer for large bodies.