codex-copilot-dx 0.4.0 → 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.
- package/README.md +10 -0
- package/bin/cli.mjs +6 -0
- package/package.json +1 -1
- package/src/adapter.mjs +38 -4
- package/src/usage.mjs +192 -0
package/README.md
CHANGED
|
@@ -47,6 +47,16 @@ Environment variables:
|
|
|
47
47
|
| `CCDX_IMG_QUALITY` | `85` | WebP quality used when re-encoding images |
|
|
48
48
|
| `CCDX_IMG_MIN_BYTES` | `100000` | Images smaller than this are left untouched |
|
|
49
49
|
| `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
|
|
50
|
+
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
|
|
51
|
+
| `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
|
|
52
|
+
|
|
53
|
+
### Usage logging
|
|
54
|
+
|
|
55
|
+
The adapter records token usage metadata to `~/.local/share/codex-copilot-dx/usage.jsonl` when upstream responses include usage fields. It logs counts, model names, API surface, and response IDs only; it does not log prompts, completions, tool arguments, or image content.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
codex-copilot-dx usage
|
|
59
|
+
```
|
|
50
60
|
|
|
51
61
|
### Image optimization
|
|
52
62
|
|
package/bin/cli.mjs
CHANGED
|
@@ -6,9 +6,15 @@ import { ensureClaudeConfig } from "../src/claude-config.mjs";
|
|
|
6
6
|
import { startAdapter } from "../src/adapter.mjs";
|
|
7
7
|
import { refreshVSCodeVersion } from "../src/copilot.mjs";
|
|
8
8
|
import { status } from "../src/status.mjs";
|
|
9
|
+
import { printUsageSummary } from "../src/usage.mjs";
|
|
9
10
|
|
|
10
11
|
const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
|
|
11
12
|
|
|
13
|
+
if (process.argv[2] === "usage") {
|
|
14
|
+
await printUsageSummary();
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
console.log(`
|
|
13
19
|
codex-copilot-dx
|
|
14
20
|
Use Codex Desktop with GitHub Copilot
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { chatCompletions, listModels, responses as copilotResponses, responsesCo
|
|
|
4
4
|
import { webStreamLines } from "./stream.mjs";
|
|
5
5
|
import { anthropicToChat, chatToAnthropic, streamAnthropicFromLines, countTokens } from "./anthropic.mjs";
|
|
6
6
|
import { status } from "./status.mjs";
|
|
7
|
+
import { recordAnthropicUsage, recordResponsesUsage } from "./usage.mjs";
|
|
7
8
|
|
|
8
9
|
// Models that only support Responses API (not chat/completions)
|
|
9
10
|
const RESPONSES_ONLY = new Set([
|
|
@@ -81,7 +82,16 @@ function storeCompletedResponseFromSse(reqContext, data) {
|
|
|
81
82
|
try {
|
|
82
83
|
const event = JSON.parse(data);
|
|
83
84
|
const response = event.response;
|
|
84
|
-
if (response?.id && response.status === "completed")
|
|
85
|
+
if (response?.id && response.status === "completed") {
|
|
86
|
+
rememberResponseHistory(reqContext, response);
|
|
87
|
+
recordResponsesUsage({
|
|
88
|
+
surface: reqContext.surface,
|
|
89
|
+
mode: "stream",
|
|
90
|
+
model: reqContext.body?.model,
|
|
91
|
+
response,
|
|
92
|
+
event,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
85
95
|
} catch {
|
|
86
96
|
// Ignore non-JSON stream fragments.
|
|
87
97
|
}
|
|
@@ -131,7 +141,17 @@ async function proxyCopilotResponses(reqContext, req, res, upstream = copilotRes
|
|
|
131
141
|
res.writeHead(resp.status, { "Content-Type": "application/json" });
|
|
132
142
|
res.end(data);
|
|
133
143
|
if (resp.ok) {
|
|
134
|
-
try {
|
|
144
|
+
try {
|
|
145
|
+
const response = JSON.parse(data);
|
|
146
|
+
rememberResponseHistory(reqContext, response);
|
|
147
|
+
recordResponsesUsage({
|
|
148
|
+
surface: reqContext.surface,
|
|
149
|
+
mode: "json",
|
|
150
|
+
model: reqContext.body?.model,
|
|
151
|
+
response,
|
|
152
|
+
event: response,
|
|
153
|
+
});
|
|
154
|
+
} catch {}
|
|
135
155
|
}
|
|
136
156
|
}
|
|
137
157
|
}
|
|
@@ -264,6 +284,7 @@ export function startAdapter(port = 2026) {
|
|
|
264
284
|
try {
|
|
265
285
|
const parsed = JSON.parse(body);
|
|
266
286
|
const prepared = prepareResponsesRequest(parsed);
|
|
287
|
+
prepared.surface = "responses";
|
|
267
288
|
const model = parsed.model || "unknown";
|
|
268
289
|
console.log(status("info", `responses model=${model} stream=${!!parsed.stream}`));
|
|
269
290
|
if (RESPONSES_ONLY.has(model)) {
|
|
@@ -274,7 +295,10 @@ export function startAdapter(port = 2026) {
|
|
|
274
295
|
forwardToChat(chatReq, (event, data) => {
|
|
275
296
|
if (!res.headersSent) res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
276
297
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
277
|
-
if (event === "response.completed")
|
|
298
|
+
if (event === "response.completed") {
|
|
299
|
+
rememberResponseHistory(prepared, data.response);
|
|
300
|
+
recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
|
|
301
|
+
}
|
|
278
302
|
}, () => { if (!res.writableEnded) res.end(); }, (status, errMsg) => { if (!res.headersSent) res.writeHead(status || 500); res.end(errMsg); });
|
|
279
303
|
} else {
|
|
280
304
|
chatReq.stream = false;
|
|
@@ -284,6 +308,7 @@ export function startAdapter(port = 2026) {
|
|
|
284
308
|
const data = await upstream.text();
|
|
285
309
|
const resp = chatToResponses(JSON.parse(data), model);
|
|
286
310
|
rememberResponseHistory(prepared, resp);
|
|
311
|
+
recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response: resp, event: resp });
|
|
287
312
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
288
313
|
res.end(JSON.stringify(resp));
|
|
289
314
|
} catch (e) {
|
|
@@ -308,6 +333,7 @@ export function startAdapter(port = 2026) {
|
|
|
308
333
|
try {
|
|
309
334
|
const parsed = JSON.parse(body);
|
|
310
335
|
const prepared = prepareResponsesRequest(parsed);
|
|
336
|
+
prepared.surface = "responses_compact";
|
|
311
337
|
const model = parsed.model || "unknown";
|
|
312
338
|
console.log(status("info", `responses compact model=${model} stream=${!!parsed.stream}`));
|
|
313
339
|
await proxyCopilotResponses(prepared, req, res, copilotResponsesCompact);
|
|
@@ -360,16 +386,24 @@ export function startAdapter(port = 2026) {
|
|
|
360
386
|
return;
|
|
361
387
|
}
|
|
362
388
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
389
|
+
let messageId;
|
|
390
|
+
let usage;
|
|
363
391
|
await streamAnthropicFromLines(
|
|
364
392
|
webStreamLines(upstream),
|
|
365
|
-
(event, data) =>
|
|
393
|
+
(event, data) => {
|
|
394
|
+
if (event === "message_start") messageId = data.message?.id;
|
|
395
|
+
if (event === "message_delta") usage = data.usage;
|
|
396
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
397
|
+
},
|
|
366
398
|
model,
|
|
367
399
|
);
|
|
400
|
+
recordAnthropicUsage({ surface: "messages", mode: "stream", model, responseId: messageId, usage });
|
|
368
401
|
if (!res.writableEnded) res.end();
|
|
369
402
|
} else {
|
|
370
403
|
const upstream = await chatCompletions({ ...chatReq, stream: false });
|
|
371
404
|
const data = await upstream.text();
|
|
372
405
|
const anthropicMsg = chatToAnthropic(JSON.parse(data), model);
|
|
406
|
+
recordAnthropicUsage({ surface: "messages", mode: "json", model: anthropicMsg.model, responseId: anthropicMsg.id, usage: anthropicMsg.usage });
|
|
373
407
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
374
408
|
res.end(JSON.stringify(anthropicMsg));
|
|
375
409
|
}
|
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
|
+
}
|