codex-copilot-dx 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,38 @@ function unexpectedArgs(args) {
7
7
  throw new Error(`Unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`);
8
8
  }
9
9
 
10
+ function parseOptions(args, schema) {
11
+ const parsed = {};
12
+ const seen = new Set();
13
+ for (let index = 0; index < args.length; index += 1) {
14
+ const option = args[index];
15
+ const kind = schema.get(option);
16
+ if (!kind || seen.has(option)) unexpectedArgs([option]);
17
+ seen.add(option);
18
+ if (kind === "flag") {
19
+ parsed[option] = true;
20
+ continue;
21
+ }
22
+
23
+ const value = args[index + 1];
24
+ if (value === undefined || !String(value).trim() || String(value).startsWith("-")) {
25
+ throw new Error(`Missing value for ${option}`);
26
+ }
27
+ parsed[option] = value;
28
+ index += 1;
29
+ }
30
+ return parsed;
31
+ }
32
+
33
+ function checkedProfile(value, { allowAll = false } = {}) {
34
+ const profile = value || "codex";
35
+ const allowed = allowAll ? new Set(["codex", "claude", "all"]) : new Set(["codex", "claude"]);
36
+ if (!allowed.has(profile)) {
37
+ throw new Error(`Profile must be ${allowAll ? "codex, claude, or all" : "codex or claude"}: ${profile}`);
38
+ }
39
+ return profile;
40
+ }
41
+
10
42
  export function parseCliArgs(args = []) {
11
43
  const [command, ...rest] = args;
12
44
  if (!command) return { command: "start", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
@@ -26,6 +58,56 @@ export function parseCliArgs(args = []) {
26
58
  if (rest.length) unexpectedArgs(rest);
27
59
  return { command: "usage", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
28
60
  }
61
+ if (command === "models") {
62
+ const options = parseOptions(rest, new Map([["--profile", "value"]]));
63
+ return {
64
+ command: "models",
65
+ configureClaudeDesktop: false,
66
+ showRequestId: false,
67
+ online: false,
68
+ compat: false,
69
+ profile: checkedProfile(options["--profile"]),
70
+ };
71
+ }
72
+ if (command === "auth") {
73
+ const [action, ...authArgs] = rest;
74
+ if (!action) throw new Error("Missing auth action: expected status or login");
75
+ if (action === "status") {
76
+ const options = parseOptions(authArgs, new Map([["--online", "flag"]]));
77
+ return {
78
+ command: "auth",
79
+ action: "status",
80
+ profile: "",
81
+ configureClaudeDesktop: false,
82
+ showRequestId: false,
83
+ online: Boolean(options["--online"]),
84
+ compat: false,
85
+ expectedLogin: "",
86
+ reauth: false,
87
+ };
88
+ }
89
+ if (action === "login") {
90
+ const [profile, ...loginArgs] = authArgs;
91
+ if (!profile) throw new Error("Missing auth login profile: expected claude");
92
+ if (profile !== "claude") throw new Error(`Auth login profile must be claude: ${profile}`);
93
+ const options = parseOptions(loginArgs, new Map([
94
+ ["--github-login", "value"],
95
+ ["--reauth", "flag"],
96
+ ]));
97
+ return {
98
+ command: "auth",
99
+ action: "login",
100
+ profile: "claude",
101
+ configureClaudeDesktop: false,
102
+ showRequestId: false,
103
+ online: false,
104
+ compat: false,
105
+ expectedLogin: options["--github-login"] || "",
106
+ reauth: Boolean(options["--reauth"]),
107
+ };
108
+ }
109
+ throw new Error(`Unknown auth action: ${action}`);
110
+ }
29
111
  if (command === "auto-review-model") {
30
112
  if (rest.length) unexpectedArgs(rest);
31
113
  return { command: "auto-review-model", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
@@ -46,18 +128,18 @@ export function parseCliArgs(args = []) {
46
128
  };
47
129
  }
48
130
  if (command === "doctor" || command === "--doctor") {
49
- const supported = new Set(["--online", "--compat"]);
50
- const invalid = rest.filter((arg) => !supported.has(arg));
51
- if (invalid.length) unexpectedArgs(invalid);
52
- for (const option of supported) {
53
- if (rest.filter((arg) => arg === option).length > 1) unexpectedArgs([option]);
54
- }
131
+ const options = parseOptions(rest, new Map([
132
+ ["--online", "flag"],
133
+ ["--compat", "flag"],
134
+ ["--profile", "value"],
135
+ ]));
55
136
  return {
56
137
  command: "doctor",
57
138
  configureClaudeDesktop: false,
58
139
  showRequestId: false,
59
- online: rest.includes("--online"),
60
- compat: rest.includes("--compat"),
140
+ online: Boolean(options["--online"]),
141
+ compat: Boolean(options["--compat"]),
142
+ profile: checkedProfile(options["--profile"], { allowAll: true }),
61
143
  };
62
144
  }
63
145
  if (START_OPTIONS.has(command)) {
@@ -123,8 +205,11 @@ export function cliHelp(commandName = "ccdx") {
123
205
  const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
124
206
  return `Usage:
125
207
  ${name} [--configure-claude-desktop] [--show-request-id]
126
- ${name} doctor [--online] [--compat]
208
+ ${name} auth status [--online]
209
+ ${name} auth login claude [--github-login <login>] [--reauth]
210
+ ${name} doctor [--online] [--compat] [--profile codex|claude|all]
127
211
  ${name} status
212
+ ${name} models [--profile codex|claude]
128
213
  ${name} usage
129
214
  ${name} auto-review-model
130
215
  ${name} update [npm|github]
@@ -51,6 +51,25 @@ function cacheHitRate(cache = {}) {
51
51
  return `${((hits / (hits + misses)) * 100).toFixed(1)}%`;
52
52
  }
53
53
 
54
+ function profileSummary(label, profile = {}) {
55
+ const client = profile.client || {};
56
+ const models = profile.models || {};
57
+ const mode = ["legacy", "inherited", "isolated"].includes(profile.mode)
58
+ ? profile.mode
59
+ : "unknown";
60
+ const source = typeof models.source === "string" && models.source ? models.source : "unknown";
61
+ const token = client.token_cached
62
+ ? `token cached (${duration(client.token_expires_in_ms)} remaining)`
63
+ : "token not cached";
64
+ return `${label} ${mode}: ${token}, ${count(models.models)} total/${count(models.claude_models)} Claude models (${source})`;
65
+ }
66
+
67
+ function routingTarget(value) {
68
+ if (value === "codex") return "Codex";
69
+ if (value === "claude") return "Claude";
70
+ return "unknown";
71
+ }
72
+
54
73
  export async function readAdapterStatus({
55
74
  host = "127.0.0.1",
56
75
  port = 2026,
@@ -107,7 +126,7 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
107
126
  ? status("ok", `Adapter ${data.version} is running at ${baseUrl} (PID ${data.pid}, uptime ${duration(data.uptime_ms)})`)
108
127
  : status("warn", `Adapter ${data.version} is running at ${baseUrl}, but this CLI is ${cliVersion}; stop the running adapter before switching versions`);
109
128
 
110
- return [
129
+ const lines = [
111
130
  `${commandName} status`,
112
131
  adapterLine,
113
132
  status("info", `Requests: ${count(requests.completed)}/${count(requests.total)} completed, ${count(requests.active)} active, ${count(requests.status_4xx)} 4xx, ${count(requests.status_5xx)} 5xx, ${count(requests.aborted)} aborted`),
@@ -117,6 +136,13 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
117
136
  status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
118
137
  status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
119
138
  status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`),
120
- status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`),
121
- ].join("\n");
139
+ ];
140
+ if (data.profiles?.codex || data.profiles?.claude) {
141
+ lines.push(status("info", `Profiles: ${profileSummary("Codex", data.profiles?.codex)}; ${profileSummary("Claude", data.profiles?.claude)}`));
142
+ }
143
+ if (data.routing) {
144
+ lines.push(status("info", `Routing: /v1/responses -> ${routingTarget(data.routing.responses)}; /v1/messages -> ${routingTarget(data.routing.messages)}`));
145
+ }
146
+ lines.push(status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`));
147
+ return lines.join("\n");
122
148
  }
@@ -0,0 +1,226 @@
1
+ import { createCopilotTokenSession } from "./copilot-token-session.mjs";
2
+ import { prepareResponsesPayload, summarizeReqBody } from "./image-optimization.mjs";
3
+ import { debugLog } from "./log.mjs";
4
+ import { enforceResponsesPayloadByteBudget } from "./responses-byte-budget.mjs";
5
+ import { status } from "./status.mjs";
6
+ import { withChatStreamUsage } from "./stream-contract.mjs";
7
+ import { markUpstreamStarted } from "./stream-performance.mjs";
8
+
9
+ function abortError(signal) {
10
+ if (signal?.reason instanceof Error) return signal.reason;
11
+ const error = new Error("The operation was aborted");
12
+ error.name = "AbortError";
13
+ error.code = "ABORT_ERR";
14
+ return error;
15
+ }
16
+
17
+ function waitForSingleflight(flight, signal) {
18
+ if (signal?.aborted) return Promise.reject(abortError(signal));
19
+ flight.waiters += 1;
20
+ return new Promise((resolve, reject) => {
21
+ let finished = false;
22
+ const finish = (handler, value) => {
23
+ if (finished) return;
24
+ finished = true;
25
+ signal?.removeEventListener("abort", onAbort);
26
+ flight.waiters -= 1;
27
+ if (!flight.settled && flight.waiters === 0) flight.controller.abort();
28
+ handler(value);
29
+ };
30
+ const onAbort = () => finish(reject, abortError(signal));
31
+ signal?.addEventListener("abort", onAbort, { once: true });
32
+ if (signal?.aborted) onAbort();
33
+ flight.promise.then(
34
+ (value) => finish(resolve, value),
35
+ (error) => finish(reject, error),
36
+ );
37
+ });
38
+ }
39
+
40
+ export function createCopilotClientRuntime({
41
+ profile = "codex",
42
+ defaultApiBase,
43
+ parseApiBase,
44
+ fetchCopilotUpstream,
45
+ buildHeaders,
46
+ getVSCodeVersion,
47
+ computeInitiator,
48
+ computeVision,
49
+ responsesEndpointPath,
50
+ ...sessionOptions
51
+ } = {}) {
52
+ const tokenSession = createCopilotTokenSession({
53
+ ...sessionOptions,
54
+ profile,
55
+ defaultApiBase,
56
+ parseApiBase,
57
+ fetchCopilotUpstream,
58
+ });
59
+ let modelEndpointCache = new Map();
60
+ const modelListFlights = new Map();
61
+
62
+ function cacheModelEndpoints(models) {
63
+ const data = Array.isArray(models) ? models : models?.data;
64
+ if (!Array.isArray(data)) return false;
65
+ const next = new Map();
66
+ for (const model of data) {
67
+ const id = String(model?.id || "").trim();
68
+ if (id && Array.isArray(model?.supported_endpoints)) next.set(id, [...model.supported_endpoints]);
69
+ }
70
+ if (!next.size) return false;
71
+ modelEndpointCache = next;
72
+ return true;
73
+ }
74
+
75
+ function getCachedModelEndpoints(modelId) {
76
+ return modelEndpointCache.get(String(modelId || "").trim()) || null;
77
+ }
78
+
79
+ function resetModelEndpointCacheForTests() {
80
+ modelEndpointCache.clear();
81
+ }
82
+
83
+ async function chatCompletions(chatReq, {
84
+ signal,
85
+ fetchImpl,
86
+ retryOptions,
87
+ bodyText,
88
+ } = {}) {
89
+ const token = await tokenSession.getToken({ signal });
90
+ const upstreamReq = chatReq.stream === true ? withChatStreamUsage(chatReq) : chatReq;
91
+ const messages = upstreamReq.messages || [];
92
+ const serializedBody = typeof bodyText === "string" && upstreamReq === chatReq
93
+ ? bodyText
94
+ : JSON.stringify(upstreamReq);
95
+ const headers = buildHeaders({
96
+ token,
97
+ version: getVSCodeVersion(),
98
+ initiator: computeInitiator(messages),
99
+ vision: computeVision(messages),
100
+ });
101
+ headers["Content-Length"] = String(Buffer.byteLength(serializedBody));
102
+ if (upstreamReq.stream === true) markUpstreamStarted();
103
+ return fetchCopilotUpstream(`${tokenSession.getApiBase()}/chat/completions`, {
104
+ method: "POST",
105
+ headers,
106
+ body: serializedBody,
107
+ signal,
108
+ }, { fetchImpl, ...retryOptions });
109
+ }
110
+
111
+ async function fetchModels({ signal, fetchImpl, retryOptions }) {
112
+ const token = await tokenSession.getToken({ signal });
113
+ const headers = buildHeaders({ token, version: getVSCodeVersion(), initiator: "user", vision: false });
114
+ const response = await fetchCopilotUpstream(`${tokenSession.getApiBase()}/models`, {
115
+ headers,
116
+ signal,
117
+ }, { fetchImpl, ...retryOptions });
118
+ const body = await response.text();
119
+ if (response.ok) {
120
+ try { cacheModelEndpoints(JSON.parse(body)); } catch {}
121
+ }
122
+ return { status: response.status, body };
123
+ }
124
+
125
+ function listModels({ signal, fetchImpl = fetch, retryOptions } = {}) {
126
+ if (signal?.aborted) return Promise.reject(abortError(signal));
127
+ let flight = modelListFlights.get(fetchImpl);
128
+ if (flight?.controller.signal.aborted) {
129
+ modelListFlights.delete(fetchImpl);
130
+ flight = null;
131
+ }
132
+ if (!flight) {
133
+ const controller = new AbortController();
134
+ flight = { controller, waiters: 0, settled: false, promise: null };
135
+ flight.promise = fetchModels({ signal: controller.signal, fetchImpl, retryOptions })
136
+ .finally(() => {
137
+ flight.settled = true;
138
+ if (modelListFlights.get(fetchImpl) === flight) modelListFlights.delete(fetchImpl);
139
+ });
140
+ modelListFlights.set(fetchImpl, flight);
141
+ }
142
+ return waitForSingleflight(flight, signal);
143
+ }
144
+
145
+ function resetModelListSingleflightForTests() {
146
+ for (const flight of modelListFlights.values()) flight.controller.abort();
147
+ modelListFlights.clear();
148
+ }
149
+
150
+ async function responses(reqBody, {
151
+ signal,
152
+ fetchImpl,
153
+ retryOptions,
154
+ currentInputStart = 0,
155
+ payloadPrepared = false,
156
+ payloadOptions = {},
157
+ } = {}) {
158
+ const token = await tokenSession.getToken({ signal });
159
+ const preparedPayload = await prepareResponsesPayload(reqBody, {
160
+ ...payloadOptions,
161
+ currentInputStart,
162
+ profiles: payloadPrepared ? [] : payloadOptions.profiles,
163
+ skipInitialOptimization: payloadPrepared || payloadOptions.skipInitialOptimization,
164
+ signal,
165
+ });
166
+ const finalizedPayload = enforceResponsesPayloadByteBudget(reqBody, preparedPayload);
167
+ const { bodyText, bodyBytes, stage, adapted } = finalizedPayload;
168
+ const summary = finalizedPayload.stage === preparedPayload.stage
169
+ ? preparedPayload.summary
170
+ : summarizeReqBody(reqBody);
171
+ console.log(status("info", `responses payload bytes=${bodyBytes} input_items=${summary.items} images=${summary.images}`));
172
+ if (adapted) debugLog(`responses payload adapted stage=${stage} bytes=${bodyBytes}/${preparedPayload.targetBytes}`);
173
+ const headers = buildHeaders({ token, version: getVSCodeVersion(), initiator: "user", vision: false });
174
+ headers["Content-Type"] = "application/json; charset=utf-8";
175
+ headers["Content-Length"] = String(bodyBytes);
176
+ headers.Accept = reqBody.stream ? "text/event-stream" : "application/json";
177
+ try {
178
+ if (reqBody.stream === true) markUpstreamStarted();
179
+ return await fetchCopilotUpstream(`${tokenSession.getApiBase()}${responsesEndpointPath()}`, {
180
+ method: "POST",
181
+ headers,
182
+ body: bodyText,
183
+ signal,
184
+ }, { fetchImpl, ...retryOptions });
185
+ } catch (error) {
186
+ const cause = error?.cause;
187
+ const causeText = cause ? ` (${[cause.code, cause.message].filter(Boolean).join(": ")})` : "";
188
+ throw new Error(`Copilot responses fetch failed: ${error.message}${causeText}`);
189
+ }
190
+ }
191
+
192
+ function responsesCompact(reqBody, options = {}) {
193
+ return responses(reqBody, options);
194
+ }
195
+
196
+ function runtimeStatus(now = Date.now()) {
197
+ return {
198
+ ...tokenSession.runtimeStatus(now),
199
+ model_endpoint_cache_entries: modelEndpointCache.size,
200
+ model_list_flights: modelListFlights.size,
201
+ };
202
+ }
203
+
204
+ function resetForTests() {
205
+ tokenSession.resetForTests();
206
+ resetModelEndpointCacheForTests();
207
+ resetModelListSingleflightForTests();
208
+ }
209
+
210
+ return Object.freeze({
211
+ profile,
212
+ cacheModelEndpoints,
213
+ getCachedModelEndpoints,
214
+ resetModelEndpointCacheForTests,
215
+ getApiBase: tokenSession.getApiBase,
216
+ runtimeStatus,
217
+ getCopilotToken: tokenSession.getToken,
218
+ resetTokenForTests: tokenSession.resetForTests,
219
+ chatCompletions,
220
+ listModels,
221
+ resetModelListSingleflightForTests,
222
+ responses,
223
+ responsesCompact,
224
+ resetForTests,
225
+ });
226
+ }