codex-copilot-dx 0.6.4 → 0.6.6
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 +20 -20
- package/bin/cli.mjs +1 -0
- package/package.json +1 -1
- package/scripts/benchmark.mjs +58 -1
- package/src/adapter.mjs +4 -5
- package/src/auto-review-model.mjs +1 -1
- package/src/cli-auth.mjs +2 -11
- package/src/cli-models.mjs +14 -7
- package/src/cli-options.mjs +1 -1
- package/src/copilot-client.mjs +3 -1
- package/src/doctor.mjs +9 -8
- package/src/http-transport.mjs +114 -13
- package/src/image-optimization.mjs +33 -11
- package/src/launcher.mjs +2 -3
- package/src/legacy-command-warning.mjs +3 -2
- package/src/models.mjs +14 -10
- package/src/observability.mjs +18 -1
- package/src/pm-studio-setup.mjs +28 -4
- package/src/pm-studio-status.mjs +103 -9
- package/src/profile-routing.mjs +7 -16
- package/src/profile-runtime.mjs +12 -3
- package/src/response-history.mjs +12 -2
- package/src/responses-byte-budget.mjs +53 -26
- package/src/responses-chat-payload.mjs +35 -16
- package/src/responses-content.mjs +36 -4
- package/src/responses-handler.mjs +47 -27
- package/src/responses-image-limit.mjs +16 -2
- package/src/responses-image-pressure.mjs +10 -4
- package/src/responses-request.mjs +28 -10
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ active account or profile. GPT requests continue to use the bearer supplied by
|
|
|
111
111
|
the current PM profile; exact enabled Anthropic model IDs use the isolated
|
|
112
112
|
CCDX Claude profile. Neither credential is copied into the other application.
|
|
113
113
|
|
|
114
|
-
The
|
|
114
|
+
The current patch recipe supports only an exact, unmodified PM Studio 2.9.7 bundle on
|
|
115
115
|
macOS. Authorize the isolated Claude profile first, quit PM Studio and its
|
|
116
116
|
updater, then run the one-time setup command:
|
|
117
117
|
|
|
@@ -120,14 +120,14 @@ ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
|
120
120
|
ccdx pms setup
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
Inspect the installed version, exact patch
|
|
124
|
-
and relay without changing files:
|
|
123
|
+
Inspect the installed version, exact patch manifest plus executable/signing
|
|
124
|
+
integrity, isolated Claude profile, and live relay routing without changing files:
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
127
|
ccdx pms status
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
`pm-studio status` and `pm-studio setup` are equivalent long-form aliases.
|
|
130
|
+
`ccdx pm-studio status` and `ccdx pm-studio setup` are equivalent long-form aliases.
|
|
131
131
|
|
|
132
132
|
`ccdx pms setup` performs only local preflight, backup, staging, integrity,
|
|
133
133
|
signing, and replacement work. It never starts Device Flow, the adapter,
|
|
@@ -187,13 +187,13 @@ To change the model used by Codex Auto-review, run:
|
|
|
187
187
|
ccdx auto-review-model
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
The selector
|
|
191
|
-
model
|
|
190
|
+
The selector first queries the running adapter, including its last-known-good
|
|
191
|
+
model list when live refresh is unavailable, then falls back to the non-expired
|
|
192
|
+
local model cache. It offers only enabled models that advertise a Responses endpoint.
|
|
192
193
|
It saves the selection in `~/.config/codex-copilot-dx/config.json` (or under
|
|
193
194
|
`XDG_CONFIG_HOME` when set); choosing `gpt-5.5` clears the override and restores
|
|
194
|
-
the package default. A running
|
|
195
|
-
|
|
196
|
-
be restarted.
|
|
195
|
+
the package default. A running adapter reads the setting on the next Auto-review
|
|
196
|
+
request, so it does not need to be restarted.
|
|
197
197
|
|
|
198
198
|
To update the globally installed package, choose a source interactively:
|
|
199
199
|
|
|
@@ -213,26 +213,26 @@ ccdx update github
|
|
|
213
213
|
The npm source installs `codex-copilot-dx@latest` through the registry already
|
|
214
214
|
configured for npm, including a company mirror. The GitHub source installs the
|
|
215
215
|
latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetching
|
|
216
|
-
for that command
|
|
217
|
-
|
|
216
|
+
for that command. It does not change npm's persistent `allow-git` setting. Both
|
|
217
|
+
paths use npm's global installer without shell
|
|
218
218
|
interpolation. A currently running adapter keeps its loaded version until it is
|
|
219
219
|
stopped and started again.
|
|
220
220
|
If a configured npm mirror has not synchronized the current release yet, use
|
|
221
221
|
the GitHub source to install the current `main` revision.
|
|
222
222
|
|
|
223
|
-
On
|
|
224
|
-
1.
|
|
225
|
-
2.
|
|
226
|
-
3.
|
|
223
|
+
On a normal launch, it will:
|
|
224
|
+
1. Print the local package version and check for a newer npm release in the background
|
|
225
|
+
2. Reuse a compatible running adapter when available; otherwise authenticate with GitHub if needed, after first trying compatible local Copilot token sources
|
|
226
|
+
3. When starting a new adapter, refresh Copilot model metadata and listen on loopback (`127.0.0.1:2026`)
|
|
227
227
|
4. Configure Codex (`~/.codex/config.toml`) to use the adapter, including stale shell env base URLs if present
|
|
228
228
|
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
|
|
229
|
-
6.
|
|
229
|
+
6. On macOS, attempt to launch Codex or ChatGPT unless `CCDX_AUTO_LAUNCH` disables it
|
|
230
230
|
|
|
231
231
|
Claude Code picks up the new `ANTHROPIC_BASE_URL` on its next launch.
|
|
232
232
|
|
|
233
|
-
If
|
|
233
|
+
If a compatible CCDX adapter is already running on the configured host and port, a second launch reuses it instead of starting another proxy. The second launch still refreshes Codex and Claude Code config, then exits.
|
|
234
234
|
|
|
235
|
-
The running adapter reports its package and protocol versions. After upgrading
|
|
235
|
+
The running adapter reports its package and protocol versions. After upgrading the package, stop the old process before starting the new version; the new CLI refuses to silently reuse an incompatible adapter.
|
|
236
236
|
|
|
237
237
|
Do not set Claude Code by manually exporting `ANTHROPIC_BASE_URL` or `ANTHROPIC_AUTH_TOKEN` in your shell. Let `ccdx` write the local config files instead. If you previously exported those variables, remove them from shell startup files and restart the terminal before launching Claude Code.
|
|
238
238
|
|
|
@@ -337,7 +337,7 @@ Environment variables:
|
|
|
337
337
|
| `CCDX_REQUEST_BODY_TIMEOUT_MS` | `120000` | Maximum time to receive and decode one request body before returning `408` |
|
|
338
338
|
| `CCDX_MAX_UPSTREAM_BODY_BYTES` | `31457280` | Strict maximum forwarded Responses body size; larger payloads are adapted locally or rejected with `413` |
|
|
339
339
|
| `CCDX_MAX_SSE_BUFFER_BYTES` | `8388608` | Maximum buffered bytes for one unterminated upstream SSE line/event |
|
|
340
|
-
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` |
|
|
340
|
+
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Per-phase timeout for Responses preparation and non-streaming upstream Copilot requests |
|
|
341
341
|
| `CCDX_STREAM_HANDSHAKE_TIMEOUT_MS` | `120000` | Timeout while waiting for upstream streaming response headers |
|
|
342
342
|
| `CCDX_STREAM_IDLE_TIMEOUT_MS` | `120000` | Maximum idle time between upstream streaming body chunks |
|
|
343
343
|
| `CCDX_UPSTREAM_RETRIES` | `2` | Retries for safe requests and clearly pre-connect POST failures; capped at `5` |
|
|
@@ -398,7 +398,7 @@ The final serialized UTF-8 request body is measured before forwarding. Above the
|
|
|
398
398
|
|
|
399
399
|
Completed image transforms are reused from a byte-bounded process-local LRU cache, and concurrent requests for the same transform share one encode. The cache key includes source content, MIME type, model-aware dimensions, WebP quality, and encoder settings; failures are not cached and request cancellation remains isolated.
|
|
400
400
|
|
|
401
|
-
Before image encoding, CCDX leaves ordinary visual history byte-for-byte unchanged while it has at most 24 historical images and the expanded body is at most 18 MiB. Above either threshold, the temporary upstream view keeps every current-turn image and at most 16 recent historical images, targeting 16 MiB. If that history tree then times out during local preparation or upstream
|
|
401
|
+
Before image encoding, CCDX leaves ordinary visual history byte-for-byte unchanged while it has at most 24 historical images and the expanded body is at most 18 MiB. Above either threshold, the temporary upstream view keeps every current-turn image and at most 16 recent historical images, targeting 16 MiB. If that history tree then times out during local preparation, a streaming handshake, or a non-streaming upstream request, or if the upstream returns HTTP 408, the next retry automatically uses an 8-image, 10 MiB recovery window for ten minutes. Two successful requests or a successful compaction clear recovery mode. CCDX never transparently repeats the ambiguous timed-out POST, and no local history is deleted; retrying does not require restarting the service.
|
|
402
402
|
|
|
403
403
|
When expanded Responses history exceeds Copilot's 50-image request limit, the adapter removes older duplicate image occurrences first, then keeps the 50 most recent images. This applies only to the forwarded request: local history remains complete, current images are preferred over older history, and requests at or below the limit are unchanged.
|
|
404
404
|
|
package/bin/cli.mjs
CHANGED
|
@@ -316,6 +316,7 @@ try {
|
|
|
316
316
|
codexModelRegistry: profileModels.codexRegistry,
|
|
317
317
|
claudeModelRegistry: profileModels.claudeRegistry,
|
|
318
318
|
claudeMode: profileRuntime.claudeMode,
|
|
319
|
+
isClaudeProfileCurrent: profileRuntime.isClaudeProfileCurrent,
|
|
319
320
|
showRequestId: CLI.showRequestId,
|
|
320
321
|
upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
|
|
321
322
|
streamHandshakeTimeoutMs: RUNTIME.streamHandshakeTimeoutMs,
|
package/package.json
CHANGED
package/scripts/benchmark.mjs
CHANGED
|
@@ -23,6 +23,14 @@ function runProbe(source) {
|
|
|
23
23
|
return JSON.parse(result.stdout.trim());
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function median(values) {
|
|
27
|
+
const ordered = [...values].sort((left, right) => left - right);
|
|
28
|
+
const middle = Math.floor(ordered.length / 2);
|
|
29
|
+
return ordered.length % 2 === 0
|
|
30
|
+
? (ordered[middle - 1] + ordered[middle]) / 2
|
|
31
|
+
: ordered[middle];
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function deterministicPixels(byteLength, initialSeed) {
|
|
27
35
|
const pixels = Buffer.alloc(byteLength);
|
|
28
36
|
let seed = initialSeed;
|
|
@@ -154,7 +162,7 @@ function largePayloadProbe(targetMiB, concurrency) {
|
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
function largeTokenCountProbe(mode) {
|
|
157
|
-
|
|
165
|
+
const samples = Array.from({ length: 3 }, () => runProbe(`
|
|
158
166
|
const text = "a ".repeat(2 * 1024 * 1024);
|
|
159
167
|
const started = performance.now();
|
|
160
168
|
let tokens;
|
|
@@ -174,6 +182,51 @@ function largeTokenCountProbe(mode) {
|
|
|
174
182
|
rss_mib: +(memory.rss / 1048576).toFixed(1),
|
|
175
183
|
heap_used_mib: +(memory.heapUsed / 1048576).toFixed(1),
|
|
176
184
|
}));
|
|
185
|
+
`));
|
|
186
|
+
return {
|
|
187
|
+
input_mib: samples[0].input_mib,
|
|
188
|
+
tokens: samples[0].tokens,
|
|
189
|
+
elapsed_ms: +median(samples.map((sample) => sample.elapsed_ms)).toFixed(1),
|
|
190
|
+
rss_mib: +median(samples.map((sample) => sample.rss_mib)).toFixed(1),
|
|
191
|
+
heap_used_mib: +median(samples.map((sample) => sample.heap_used_mib)).toFixed(1),
|
|
192
|
+
samples: samples.length,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toolOutputParseCacheProbe() {
|
|
197
|
+
return runProbe(`
|
|
198
|
+
import { enforceResponsesImageLimit } from "./src/responses-image-limit.mjs";
|
|
199
|
+
import { responsesHistoricalImageStats } from "./src/responses-byte-budget.mjs";
|
|
200
|
+
import { prepareResponsesChatPayload } from "./src/responses-chat-payload.mjs";
|
|
201
|
+
|
|
202
|
+
const item = {
|
|
203
|
+
type: "function_call_output",
|
|
204
|
+
call_id: "call_benchmark",
|
|
205
|
+
output: JSON.stringify([{ type: "input_text", text: "x".repeat(8 * 1024 * 1024) }]),
|
|
206
|
+
};
|
|
207
|
+
const source = item.output;
|
|
208
|
+
const originalParse = JSON.parse;
|
|
209
|
+
let parseCalls = 0;
|
|
210
|
+
JSON.parse = function countedParse(value, ...args) {
|
|
211
|
+
if (value === source) parseCalls += 1;
|
|
212
|
+
return originalParse.call(this, value, ...args);
|
|
213
|
+
};
|
|
214
|
+
const started = performance.now();
|
|
215
|
+
try {
|
|
216
|
+
enforceResponsesImageLimit([item]);
|
|
217
|
+
responsesHistoricalImageStats([item], 1);
|
|
218
|
+
await prepareResponsesChatPayload({
|
|
219
|
+
body: { model: "gpt-4o", input: [item] },
|
|
220
|
+
currentInputStart: 1,
|
|
221
|
+
}, { payloadOptions: { maxBytes: 30 * 1024 * 1024 } });
|
|
222
|
+
} finally {
|
|
223
|
+
JSON.parse = originalParse;
|
|
224
|
+
}
|
|
225
|
+
process.stdout.write(JSON.stringify({
|
|
226
|
+
output_mib: +(Buffer.byteLength(source) / 1048576).toFixed(1),
|
|
227
|
+
parse_calls: parseCalls,
|
|
228
|
+
elapsed_ms: +(performance.now() - started).toFixed(1),
|
|
229
|
+
}));
|
|
177
230
|
`);
|
|
178
231
|
}
|
|
179
232
|
|
|
@@ -373,6 +426,7 @@ if (checkMode) {
|
|
|
373
426
|
proxy: largeTokenCountProbe("proxy"),
|
|
374
427
|
materialized_array: largeTokenCountProbe("array"),
|
|
375
428
|
};
|
|
429
|
+
report.tool_output_parse_cache = toolOutputParseCacheProbe();
|
|
376
430
|
report.large_payload_peak = [
|
|
377
431
|
largePayloadProbe(5, 4),
|
|
378
432
|
largePayloadProbe(30, 1),
|
|
@@ -409,6 +463,9 @@ if (checkMode) {
|
|
|
409
463
|
if (proxy.heap_used_mib > array.heap_used_mib * 0.75) {
|
|
410
464
|
failures.push(`token counter heap regression: proxy=${proxy.heap_used_mib}MiB array=${array.heap_used_mib}MiB`);
|
|
411
465
|
}
|
|
466
|
+
if (report.tool_output_parse_cache.parse_calls !== 1) {
|
|
467
|
+
failures.push(`stringified tool output parsed ${report.tool_output_parse_cache.parse_calls} times instead of once`);
|
|
468
|
+
}
|
|
412
469
|
for (const payload of report.large_payload_peak) {
|
|
413
470
|
if (payload.admission_max_active_mib > payload.admission_budget_mib) {
|
|
414
471
|
failures.push(`request admission exceeded its budget at ${payload.target_request_mib}MiB x${payload.concurrency}`);
|
package/src/adapter.mjs
CHANGED
|
@@ -215,6 +215,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
215
215
|
codexModelRegistry,
|
|
216
216
|
claudeModelRegistry,
|
|
217
217
|
claudeMode,
|
|
218
|
+
isClaudeProfileCurrent: options.isClaudeProfileCurrent,
|
|
218
219
|
})));
|
|
219
220
|
return;
|
|
220
221
|
}
|
|
@@ -366,11 +367,9 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
|
366
367
|
server.once("close", cleanupTerminalActivity);
|
|
367
368
|
|
|
368
369
|
server.on("upgrade", (req, socket) => {
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// 5-attempt reconnect storm. Refuse the upgrade so Codex falls back to
|
|
373
|
-
// plain HTTP SSE, which this adapter handles correctly.
|
|
370
|
+
// The responses_websockets upgrade expects a server-push protocol that this
|
|
371
|
+
// adapter does not implement. Refuse it so the client can fall back to the
|
|
372
|
+
// supported HTTP SSE transport instead of waiting on an unusable socket.
|
|
374
373
|
socket.write("HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
375
374
|
socket.destroy();
|
|
376
375
|
});
|
|
@@ -135,7 +135,7 @@ export async function runAutoReviewModelCommand({
|
|
|
135
135
|
if (String(env.CCDX_AUTO_REVIEW_MODEL || "").trim()) {
|
|
136
136
|
output.write(`CCDX_AUTO_REVIEW_MODEL=${current.model} remains the effective override until it is unset.\n`);
|
|
137
137
|
} else {
|
|
138
|
-
output.write("The running
|
|
138
|
+
output.write("The running adapter will use this model on the next Auto Review request.\n");
|
|
139
139
|
}
|
|
140
140
|
return { ...result, cancelled: false, model: selectedModel };
|
|
141
141
|
}
|
package/src/cli-auth.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
normalizeGithubIdentity,
|
|
22
22
|
} from "./github-identity.mjs";
|
|
23
23
|
import { saveModelCache } from "./model-cache.mjs";
|
|
24
|
+
import { isClaudeCopilotModel } from "./models.mjs";
|
|
24
25
|
import { profileRouting } from "./profile-routing.mjs";
|
|
25
26
|
import { status } from "./status.mjs";
|
|
26
27
|
|
|
@@ -145,17 +146,7 @@ async function codexIdentity({ home, fetchImpl, signal }) {
|
|
|
145
146
|
function claudeModels(payload) {
|
|
146
147
|
const models = Array.isArray(payload) ? payload : payload?.data;
|
|
147
148
|
if (!Array.isArray(models)) throw new Error("Claude account model catalog contained no model list");
|
|
148
|
-
return models.filter(
|
|
149
|
-
const id = String(model?.id || "").trim();
|
|
150
|
-
const vendor = String(model?.vendor || model?.owned_by || "").trim().toLowerCase();
|
|
151
|
-
const policy = String(model?.policy?.state || "").trim().toLowerCase();
|
|
152
|
-
const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
|
|
153
|
-
return Boolean(id)
|
|
154
|
-
&& (id.toLowerCase().startsWith("claude-") || vendor === "anthropic")
|
|
155
|
-
&& model?.model_picker_enabled === true
|
|
156
|
-
&& (!policy || policy === "enabled")
|
|
157
|
-
&& (endpoints.includes("/chat/completions") || endpoints.includes("/v1/messages"));
|
|
158
|
-
});
|
|
149
|
+
return models.filter(isClaudeCopilotModel);
|
|
159
150
|
}
|
|
160
151
|
|
|
161
152
|
function catalogModels(payload) {
|
package/src/cli-models.mjs
CHANGED
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
readAuthProfileCredentials,
|
|
7
7
|
} from "./auth-profile.mjs";
|
|
8
8
|
import { buildHeaders, DEFAULT_API_BASE, FALLBACK_VSCODE_VERSION } from "./copilot.mjs";
|
|
9
|
+
import {
|
|
10
|
+
isClaudeCopilotCatalogEntry,
|
|
11
|
+
isClaudeCopilotModel,
|
|
12
|
+
} from "./models.mjs";
|
|
9
13
|
import { status } from "./status.mjs";
|
|
10
14
|
|
|
11
15
|
const DEFAULT_MODELS_TIMEOUT_MS = 10000;
|
|
@@ -37,11 +41,15 @@ export function selectableCopilotModels(payload) {
|
|
|
37
41
|
const models = [];
|
|
38
42
|
const seen = new Set();
|
|
39
43
|
for (const model of data) {
|
|
40
|
-
if (model
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
if (isClaudeCopilotCatalogEntry(model)) {
|
|
45
|
+
if (!isClaudeCopilotModel(model)) continue;
|
|
46
|
+
} else {
|
|
47
|
+
if (model?.model_picker_enabled !== true) continue;
|
|
48
|
+
const policyState = safeText(model?.policy?.state).toLowerCase();
|
|
49
|
+
if (policyState && policyState !== "enabled") continue;
|
|
50
|
+
const capabilityType = safeText(model?.capabilities?.type).toLowerCase();
|
|
51
|
+
if (capabilityType && capabilityType !== "chat") continue;
|
|
52
|
+
}
|
|
45
53
|
|
|
46
54
|
const id = safeText(model?.id);
|
|
47
55
|
const endpoints = endpointLabels(model);
|
|
@@ -163,8 +171,7 @@ export function formatLiveCopilotModels(catalog, { commandName = "ccdx" } = {})
|
|
|
163
171
|
const profile = catalog?.profile === AUTH_PROFILE_CLAUDE ? AUTH_PROFILE_CLAUDE : AUTH_PROFILE_CODEX;
|
|
164
172
|
const responses = models.filter((model) => model.endpoints.includes("responses")).length;
|
|
165
173
|
const chat = models.filter((model) => model.endpoints.includes("chat")).length;
|
|
166
|
-
const claude = models.filter(
|
|
167
|
-
|| model.vendor.toLowerCase() === "anthropic").length;
|
|
174
|
+
const claude = models.filter(isClaudeCopilotCatalogEntry).length;
|
|
168
175
|
const lines = [
|
|
169
176
|
`${commandName} models${profile === AUTH_PROFILE_CLAUDE ? " --profile claude" : ""}`,
|
|
170
177
|
status("ok", `Live catalog from ${safeText(catalog?.upstreamHost, "GitHub Copilot")}: ${models.length} selectable of ${Number(catalog?.advertised) || 0} advertised`),
|
package/src/cli-options.mjs
CHANGED
|
@@ -258,7 +258,7 @@ export function cliCommandName() {
|
|
|
258
258
|
|
|
259
259
|
function topicHelp(name, topic) {
|
|
260
260
|
const sections = {
|
|
261
|
-
start: `Usage:\n ${name} [start] [--configure-claude-app] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex and Claude Code configuration, and
|
|
261
|
+
start: `Usage:\n ${name} [start] [--configure-claude-app] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex and Claude Code configuration, and attempts to open Codex or ChatGPT when auto-launch is enabled and supported.\n--configure-claude-app also creates or updates the managed Claude App gateway profile.`,
|
|
262
262
|
auth: `Usage:\n ${name} auth status [--online]\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nShows the two account profiles or configures the isolated Claude account. Device login starts only when no reusable local Copilot credential is available or --reauth is used.`,
|
|
263
263
|
"auth status": `Usage:\n ${name} auth status [--online]\n\nShows account routing without exposing credentials. --online also verifies both configured Copilot entitlements and model catalogs.`,
|
|
264
264
|
"auth login": `Usage:\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nReuses a compatible local Copilot credential when possible. --reauth forces GitHub Device Flow.`,
|
package/src/copilot-client.mjs
CHANGED
|
@@ -150,6 +150,7 @@ export function createCopilotClientRuntime({
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
async function responses(reqBody, {
|
|
153
|
+
assertActive,
|
|
153
154
|
signal,
|
|
154
155
|
fetchImpl,
|
|
155
156
|
retryOptions,
|
|
@@ -160,13 +161,14 @@ export function createCopilotClientRuntime({
|
|
|
160
161
|
} = {}) {
|
|
161
162
|
const token = await tokenSession.getToken({ signal });
|
|
162
163
|
const preparedPayload = await prepareResponsesPayload(reqBody, {
|
|
164
|
+
assertActive,
|
|
163
165
|
...payloadOptions,
|
|
164
166
|
currentInputStart,
|
|
165
167
|
profiles: payloadPrepared ? [] : payloadOptions.profiles,
|
|
166
168
|
skipInitialOptimization: payloadPrepared || payloadOptions.skipInitialOptimization,
|
|
167
169
|
signal,
|
|
168
170
|
});
|
|
169
|
-
const finalizedPayload = enforceResponsesPayloadByteBudget(reqBody, preparedPayload);
|
|
171
|
+
const finalizedPayload = enforceResponsesPayloadByteBudget(reqBody, preparedPayload, { assertActive });
|
|
170
172
|
const { bodyText, bodyBytes, stage, adapted } = finalizedPayload;
|
|
171
173
|
const summary = finalizedPayload.stage === preparedPayload.stage
|
|
172
174
|
? preparedPayload.summary
|
package/src/doctor.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { claudeDesktopPaths } from "./claude-desktop-config.mjs";
|
|
|
12
12
|
import { status } from "./status.mjs";
|
|
13
13
|
import { buildHeaders, DEFAULT_API_BASE, FALLBACK_VSCODE_VERSION } from "./copilot.mjs";
|
|
14
14
|
import { adapterBaseUrl, checkRunningAdapter } from "./running-adapter.mjs";
|
|
15
|
-
import { CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
15
|
+
import { CODEX_AUTO_REVIEW_MODEL, isClaudeCopilotModel } from "./models.mjs";
|
|
16
16
|
import { loadModelCache } from "./model-cache.mjs";
|
|
17
17
|
|
|
18
18
|
function localGatewayBaseUrl(host, port) {
|
|
@@ -66,7 +66,7 @@ function modelEndpoints(model) {
|
|
|
66
66
|
|
|
67
67
|
export function selectCompatibilityModels(models, { claudeModels = models } = {}) {
|
|
68
68
|
const data = copilotModelData(models).filter((model) => model?.model_picker_enabled !== false);
|
|
69
|
-
const claudeData = copilotModelData(claudeModels)
|
|
69
|
+
const claudeData = copilotModelData(claudeModels);
|
|
70
70
|
const responsesCandidates = data.filter((model) => {
|
|
71
71
|
const id = String(model?.id || "");
|
|
72
72
|
const endpoints = modelEndpoints(model);
|
|
@@ -74,12 +74,7 @@ export function selectCompatibilityModels(models, { claudeModels = models } = {}
|
|
|
74
74
|
});
|
|
75
75
|
const responsesOnly = [...responsesCandidates].reverse()
|
|
76
76
|
.find((model) => !modelEndpoints(model).includes("/chat/completions"));
|
|
77
|
-
const claude = claudeData.find(
|
|
78
|
-
const id = String(model?.id || "");
|
|
79
|
-
const vendor = String(model?.vendor || "").toLowerCase();
|
|
80
|
-
return (id.startsWith("claude-") || vendor === "anthropic")
|
|
81
|
-
&& modelEndpoints(model).includes("/chat/completions");
|
|
82
|
-
});
|
|
77
|
+
const claude = claudeData.find(isClaudeCopilotModel);
|
|
83
78
|
return {
|
|
84
79
|
responsesModel: String((responsesOnly || responsesCandidates[0])?.id || ""),
|
|
85
80
|
claudeModel: String(claude?.id || ""),
|
|
@@ -588,6 +583,12 @@ export async function collectDoctorChecks({
|
|
|
588
583
|
}
|
|
589
584
|
if (!pm.adapter?.ok) {
|
|
590
585
|
checks.push({ kind: "warn", message: "PM Studio relay is not currently available", fix: "ccdx start" });
|
|
586
|
+
} else if (!pm.runtime?.ok) {
|
|
587
|
+
checks.push({
|
|
588
|
+
kind: "warn",
|
|
589
|
+
message: "PM Studio relay is running, but its isolated routing is not ready",
|
|
590
|
+
fix: "stop the running adapter, then run ccdx start",
|
|
591
|
+
});
|
|
591
592
|
}
|
|
592
593
|
} else if (pm.app.state === "clean") {
|
|
593
594
|
checks.push({
|
package/src/http-transport.mjs
CHANGED
|
@@ -47,29 +47,56 @@ function admissionAbortError(signal) {
|
|
|
47
47
|
return error;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, label }) {
|
|
50
|
+
function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, label, blockBehindExclusive = false }) {
|
|
51
51
|
const queue = [];
|
|
52
52
|
let activeWeight = 0;
|
|
53
|
+
let activeRequestedWeight = 0;
|
|
53
54
|
let activeRequests = 0;
|
|
54
55
|
|
|
55
56
|
const activate = (entry) => {
|
|
56
57
|
entry.cleanup();
|
|
57
58
|
activeWeight += entry.weight;
|
|
59
|
+
activeRequestedWeight += entry.requestedWeight;
|
|
58
60
|
activeRequests += 1;
|
|
59
61
|
let released = false;
|
|
60
|
-
|
|
62
|
+
const release = () => {
|
|
61
63
|
if (released) return;
|
|
62
64
|
released = true;
|
|
63
65
|
activeWeight = Math.max(0, activeWeight - entry.weight);
|
|
66
|
+
activeRequestedWeight = Math.max(0, activeRequestedWeight - entry.requestedWeight);
|
|
64
67
|
activeRequests = Math.max(0, activeRequests - 1);
|
|
65
68
|
drain();
|
|
69
|
+
};
|
|
70
|
+
release.resize = (requestedWeight) => {
|
|
71
|
+
if (released) return 0;
|
|
72
|
+
const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
|
|
73
|
+
? Math.ceil(requestedWeight)
|
|
74
|
+
: maxWeight;
|
|
75
|
+
const requested = Math.max(1, parsedWeight);
|
|
76
|
+
const weight = Math.min(requested, maxWeight);
|
|
77
|
+
if (weight > entry.weight) {
|
|
78
|
+
throw new RangeError(`${label} admission weight cannot grow`);
|
|
79
|
+
}
|
|
80
|
+
activeWeight = Math.max(0, activeWeight - (entry.weight - weight));
|
|
81
|
+
activeRequestedWeight = Math.max(0, activeRequestedWeight + requested - entry.requestedWeight);
|
|
82
|
+
entry.weight = weight;
|
|
83
|
+
entry.requestedWeight = requested;
|
|
84
|
+
drain();
|
|
85
|
+
return weight;
|
|
86
|
+
};
|
|
87
|
+
Object.defineProperties(release, {
|
|
88
|
+
weight: { get: () => released ? 0 : entry.weight },
|
|
89
|
+
requestedWeight: { get: () => released ? 0 : entry.requestedWeight },
|
|
90
|
+
maxWeight: { value: maxWeight },
|
|
66
91
|
});
|
|
92
|
+
entry.resolve(release);
|
|
67
93
|
};
|
|
68
94
|
|
|
69
95
|
const drain = () => {
|
|
70
96
|
for (let index = 0; index < queue.length;) {
|
|
71
97
|
const entry = queue[index];
|
|
72
98
|
if (activeWeight + entry.weight > maxWeight) {
|
|
99
|
+
if (blockBehindExclusive && entry.weight === maxWeight) break;
|
|
73
100
|
index += 1;
|
|
74
101
|
continue;
|
|
75
102
|
}
|
|
@@ -83,8 +110,12 @@ function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, labe
|
|
|
83
110
|
const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
|
|
84
111
|
? Math.ceil(requestedWeight)
|
|
85
112
|
: maxWeight;
|
|
86
|
-
const
|
|
87
|
-
|
|
113
|
+
const requested = Math.max(1, parsedWeight);
|
|
114
|
+
const weight = Math.min(requested, maxWeight);
|
|
115
|
+
const blockedByExclusive = blockBehindExclusive
|
|
116
|
+
&& queue.some((entry) => entry.weight === maxWeight);
|
|
117
|
+
const mustWait = blockedByExclusive || activeWeight + weight > maxWeight;
|
|
118
|
+
if (queue.length >= maxQueued && mustWait) {
|
|
88
119
|
return Promise.reject(httpError(`${label} queue is full (${maxQueued} waiting)`, 503));
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -92,6 +123,7 @@ function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, labe
|
|
|
92
123
|
let timer;
|
|
93
124
|
const entry = {
|
|
94
125
|
weight,
|
|
126
|
+
requestedWeight: requested,
|
|
95
127
|
resolve,
|
|
96
128
|
cleanup: () => {
|
|
97
129
|
if (timer) clearTimeout(timer);
|
|
@@ -120,7 +152,13 @@ function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, labe
|
|
|
120
152
|
});
|
|
121
153
|
};
|
|
122
154
|
|
|
123
|
-
acquire.stats = () => ({
|
|
155
|
+
acquire.stats = () => ({
|
|
156
|
+
activeWeight,
|
|
157
|
+
activeRequestedWeight,
|
|
158
|
+
activeRequests,
|
|
159
|
+
queued: queue.length,
|
|
160
|
+
maxWeight,
|
|
161
|
+
});
|
|
124
162
|
return acquire;
|
|
125
163
|
}
|
|
126
164
|
|
|
@@ -143,6 +181,7 @@ export function createRequestAdmission({
|
|
|
143
181
|
maxQueued: queueLimit,
|
|
144
182
|
waitTimeoutMs: timeoutMs,
|
|
145
183
|
label: "Decoded request body",
|
|
184
|
+
blockBehindExclusive: true,
|
|
146
185
|
});
|
|
147
186
|
const acquireResponseHistory = createSupplementalAdmission({
|
|
148
187
|
maxWeight: byteLimit,
|
|
@@ -194,10 +233,30 @@ export function createRequestAdmission({
|
|
|
194
233
|
releaseSupplemental();
|
|
195
234
|
throw admissionAbortError(options?.signal);
|
|
196
235
|
}
|
|
197
|
-
|
|
236
|
+
let supplementalReleased = false;
|
|
237
|
+
const releaseReservation = () => {
|
|
238
|
+
if (supplementalReleased) return;
|
|
239
|
+
supplementalReleased = true;
|
|
240
|
+
supplementalReleases.delete(releaseReservation);
|
|
241
|
+
releaseSupplemental();
|
|
242
|
+
};
|
|
243
|
+
releaseReservation.resize = (nextWeight) => {
|
|
244
|
+
if (supplementalReleased) return 0;
|
|
245
|
+
return releaseSupplemental.resize(nextWeight);
|
|
246
|
+
};
|
|
247
|
+
Object.defineProperties(releaseReservation, {
|
|
248
|
+
weight: { get: () => supplementalReleased ? 0 : releaseSupplemental.weight },
|
|
249
|
+
requestedWeight: {
|
|
250
|
+
get: () => supplementalReleased ? 0 : releaseSupplemental.requestedWeight,
|
|
251
|
+
},
|
|
252
|
+
maxWeight: { value: releaseSupplemental.maxWeight },
|
|
253
|
+
});
|
|
254
|
+
supplementalReleases.add(releaseReservation);
|
|
255
|
+
return releaseReservation;
|
|
198
256
|
};
|
|
199
257
|
release.acquireDecompression = (options) => acquireDecompression(1, options);
|
|
200
258
|
release.reserveDecodedBody = (bytes, options) => reserve(acquireDecodedBody, bytes, options);
|
|
259
|
+
release.reserveDecodedBody.supportsResize = true;
|
|
201
260
|
release.reserveResponseHistory = (bytes, options) => reserve(acquireResponseHistory, bytes, options);
|
|
202
261
|
entry.resolve(release);
|
|
203
262
|
};
|
|
@@ -285,7 +344,8 @@ export function createRequestAdmission({
|
|
|
285
344
|
waitMsMax: counters.waitMsMax,
|
|
286
345
|
decompressionsActive: acquireDecompression.stats().activeRequests,
|
|
287
346
|
decompressionsQueued: acquireDecompression.stats().queued,
|
|
288
|
-
decodedBodyBytes: acquireDecodedBody.stats().
|
|
347
|
+
decodedBodyBytes: acquireDecodedBody.stats().activeRequestedWeight,
|
|
348
|
+
decodedBodyAdmissionBytes: acquireDecodedBody.stats().activeWeight,
|
|
289
349
|
decodedBodiesActive: acquireDecodedBody.stats().activeRequests,
|
|
290
350
|
decodedBodiesQueued: acquireDecodedBody.stats().queued,
|
|
291
351
|
responseHistoryBytes: acquireResponseHistory.stats().activeWeight,
|
|
@@ -445,21 +505,62 @@ export async function readJsonBody(req, {
|
|
|
445
505
|
return parseRequestJson(await readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes, signal));
|
|
446
506
|
}
|
|
447
507
|
const buffer = await readRequestBuffer(req, maxBodyBytes, signal);
|
|
448
|
-
|
|
508
|
+
let releaseDecompression = () => {};
|
|
509
|
+
let decodedReservation;
|
|
510
|
+
let retainDecodedReservation = false;
|
|
449
511
|
try {
|
|
450
512
|
let decoded;
|
|
513
|
+
const supportsResizableReservation = admission?.reserveDecodedBody?.supportsResize === true;
|
|
514
|
+
// Most compressed bodies fit the existing 4x admission estimate. High-ratio bodies retry
|
|
515
|
+
// only after obtaining the pool's exclusive reservation, so a full decoded body never waits
|
|
516
|
+
// outside the decoded-body budget.
|
|
517
|
+
const estimatedDecodedBytes = Math.max(
|
|
518
|
+
1,
|
|
519
|
+
Math.min(buffer.length * COMPRESSED_BODY_WEIGHT_MULTIPLIER, maxDecodedBodyBytes),
|
|
520
|
+
);
|
|
521
|
+
if (supportsResizableReservation) {
|
|
522
|
+
decodedReservation = await admission.reserveDecodedBody(estimatedDecodedBytes, { signal });
|
|
523
|
+
}
|
|
524
|
+
releaseDecompression = await admission?.acquireDecompression?.({ signal }) || (() => {});
|
|
525
|
+
const reservedBytes = decodedReservation?.requestedWeight;
|
|
526
|
+
const reservedAdmissionBytes = decodedReservation?.weight;
|
|
527
|
+
const decodedPoolBytes = decodedReservation?.maxWeight;
|
|
528
|
+
const tentativeDecodeLimit = Number.isFinite(reservedBytes)
|
|
529
|
+
&& Number.isFinite(reservedAdmissionBytes)
|
|
530
|
+
&& Number.isFinite(decodedPoolBytes)
|
|
531
|
+
&& reservedAdmissionBytes < decodedPoolBytes
|
|
532
|
+
? Math.min(reservedBytes, maxDecodedBodyBytes)
|
|
533
|
+
: maxDecodedBodyBytes;
|
|
451
534
|
try {
|
|
452
|
-
decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"],
|
|
535
|
+
decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], tentativeDecodeLimit);
|
|
453
536
|
} catch (e) {
|
|
454
|
-
if (e?.statusCode)
|
|
455
|
-
|
|
537
|
+
if (e?.statusCode === 413 && tentativeDecodeLimit < maxDecodedBodyBytes && supportsResizableReservation) {
|
|
538
|
+
releaseDecompression();
|
|
539
|
+
releaseDecompression = () => {};
|
|
540
|
+
decodedReservation();
|
|
541
|
+
decodedReservation = await admission.reserveDecodedBody(maxDecodedBodyBytes, { signal });
|
|
542
|
+
releaseDecompression = await admission.acquireDecompression?.({ signal }) || (() => {});
|
|
543
|
+
try {
|
|
544
|
+
decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], maxDecodedBodyBytes);
|
|
545
|
+
} catch (retryError) {
|
|
546
|
+
if (retryError?.statusCode) throw retryError;
|
|
547
|
+
throw httpError(`Invalid compressed request body: ${retryError.message}`, 400);
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
if (e?.statusCode) throw e;
|
|
551
|
+
throw httpError(`Invalid compressed request body: ${e.message}`, 400);
|
|
552
|
+
}
|
|
456
553
|
}
|
|
457
554
|
if (signal?.aborted) throw admissionAbortError(signal);
|
|
458
555
|
if (decoded.length > 0) {
|
|
459
|
-
|
|
556
|
+
if (supportsResizableReservation) decodedReservation.resize(decoded.length);
|
|
557
|
+
else decodedReservation = await admission?.reserveDecodedBody?.(decoded.length, { signal });
|
|
460
558
|
}
|
|
461
|
-
|
|
559
|
+
const parsed = parseRequestJson(decoded.toString("utf8"));
|
|
560
|
+
retainDecodedReservation = true;
|
|
561
|
+
return parsed;
|
|
462
562
|
} finally {
|
|
563
|
+
if (!retainDecodedReservation) decodedReservation?.();
|
|
463
564
|
releaseDecompression();
|
|
464
565
|
}
|
|
465
566
|
}
|