codex-copilot-dx 0.6.18 → 0.6.20
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 +2 -0
- package/bin/cli.mjs +2 -1
- package/package.json +1 -1
- package/src/adapter.mjs +19 -3
- package/src/anthropic-handler.mjs +9 -4
- package/src/anthropic.mjs +10 -3
- package/src/auth-profile.mjs +3 -3
- package/src/auth.mjs +44 -10
- package/src/auto-review-model.mjs +2 -2
- package/src/cli-auth.mjs +98 -25
- package/src/cli-error.mjs +50 -0
- package/src/cli-models.mjs +2 -2
- package/src/cli-options.mjs +10 -8
- package/src/copilot-token-session.mjs +17 -2
- package/src/copilot.mjs +22 -3
- package/src/doctor.mjs +2 -2
- package/src/http-transport.mjs +56 -4
- package/src/lock.mjs +24 -4
- package/src/pm-studio-bundle-content.mjs +165 -0
- package/src/pm-studio-relay.mjs +26 -5
- package/src/pm-studio-setup.mjs +42 -162
- package/src/responses-handler.mjs +7 -2
- package/src/responses-proxy.mjs +6 -1
- package/src/runtime-config.mjs +33 -7
- package/src/token-counter-service.mjs +211 -0
- package/src/token-counter-worker.mjs +16 -0
- package/src/usage.mjs +80 -13
package/README.md
CHANGED
|
@@ -378,6 +378,8 @@ Environment variables:
|
|
|
378
378
|
| `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
|
|
379
379
|
| `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
|
|
380
380
|
| `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Per-pool byte budget for raw bodies, decoded bodies, and materialized response history; a larger item runs exclusively in its pool |
|
|
381
|
+
| `CCDX_MAX_UPSTREAM_CHAT_RESPONSE_BYTES` | `67108864` | Maximum buffered successful Chat Completions response used by Responses, Anthropic, and PM Studio conversion paths |
|
|
382
|
+
| `CCDX_MAX_UPSTREAM_RESPONSES_RESPONSE_BYTES` | `268435456` | Maximum buffered successful native Responses or compact response; streaming Responses are forwarded with bounded backpressure instead |
|
|
381
383
|
| `CCDX_MAX_QUEUED_REQUESTS` | `16` | Maximum body requests waiting for the shared byte budget |
|
|
382
384
|
| `CCDX_REQUEST_QUEUE_TIMEOUT_MS` | `120000` | Maximum wait for request-body admission before returning `503` |
|
|
383
385
|
| `CCDX_REQUEST_BODY_TIMEOUT_MS` | `120000` | Maximum time to receive and decode one request body before returning `408` |
|
package/bin/cli.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
|
|
|
17
17
|
import { runInBackground } from "../src/startup.mjs";
|
|
18
18
|
import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
19
19
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
20
|
+
import { formatCliError } from "../src/cli-error.mjs";
|
|
20
21
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
21
22
|
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
22
23
|
import { autoReviewModelPreference } from "../src/user-settings.mjs";
|
|
@@ -152,7 +153,7 @@ if (CLI.command === "pms") {
|
|
|
152
153
|
await runPmStudioSetup({ commandName: CLI_NAME });
|
|
153
154
|
process.exit(0);
|
|
154
155
|
} catch (e) {
|
|
155
|
-
console.error(
|
|
156
|
+
console.error(formatCliError(e));
|
|
156
157
|
process.exit(1);
|
|
157
158
|
}
|
|
158
159
|
}
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { createPmStudioRelayHandler, PM_STUDIO_RELAY_PREFIX } from "./pm-studio-
|
|
|
15
15
|
import { createStreamPerformanceMetrics } from "./stream-performance.mjs";
|
|
16
16
|
import { status } from "./status.mjs";
|
|
17
17
|
import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
|
|
18
|
+
import { createTokenCounterService } from "./token-counter-service.mjs";
|
|
18
19
|
import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
19
20
|
import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
|
|
20
21
|
import { createResponsesImagePressureController } from "./responses-image-pressure.mjs";
|
|
@@ -145,6 +146,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
145
146
|
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
146
147
|
const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
|
|
147
148
|
const imagePressure = options.imagePressure || createResponsesImagePressureController();
|
|
149
|
+
const ownsTokenCounterService = !options.tokenCounterService;
|
|
150
|
+
const tokenCounterService = options.tokenCounterService || createTokenCounterService({
|
|
151
|
+
timeoutMs: options.countTokensTimeoutMs,
|
|
152
|
+
maxQueued: options.countTokensMaxQueued,
|
|
153
|
+
});
|
|
148
154
|
const claudeDesktopModelOptions = () => {
|
|
149
155
|
const modelDefs = claudeModelRegistry?.modelDefs || options.claudeDesktopModelDefs;
|
|
150
156
|
return Array.isArray(modelDefs) ? { modelDefs } : {};
|
|
@@ -178,7 +184,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
178
184
|
streamIdleTimeoutMs,
|
|
179
185
|
upstreamTimeoutMs,
|
|
180
186
|
});
|
|
181
|
-
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({
|
|
187
|
+
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({
|
|
188
|
+
acquireRequest,
|
|
189
|
+
requestBodyTimeoutMs,
|
|
190
|
+
countTokensFn: (body, countOptions) => tokenCounterService.count(body, countOptions),
|
|
191
|
+
});
|
|
182
192
|
const anthropicMessagesHandler = createAnthropicMessagesHandler({
|
|
183
193
|
acquireRequest,
|
|
184
194
|
chatCompletionsFn: claudeChatCompletionsFn,
|
|
@@ -300,7 +310,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
300
310
|
? options.dispatchRequestForTests
|
|
301
311
|
: dispatch;
|
|
302
312
|
|
|
303
|
-
|
|
313
|
+
const handler = (req, res) => {
|
|
304
314
|
const requestId = createRequestId();
|
|
305
315
|
if (typeof res.setHeader === "function") {
|
|
306
316
|
res.setHeader("X-Request-Id", requestId);
|
|
@@ -378,6 +388,10 @@ export function createAdapterHandler(options = {}) {
|
|
|
378
388
|
containUnexpectedError(error);
|
|
379
389
|
}
|
|
380
390
|
};
|
|
391
|
+
handler.cleanup = () => {
|
|
392
|
+
if (ownsTokenCounterService) tokenCounterService.close();
|
|
393
|
+
};
|
|
394
|
+
return handler;
|
|
381
395
|
}
|
|
382
396
|
|
|
383
397
|
// Public server entry point.
|
|
@@ -385,9 +399,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
385
399
|
export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
386
400
|
const ownsTerminalActivity = !Object.hasOwn(options, "terminalActivity");
|
|
387
401
|
const terminalActivity = ownsTerminalActivity ? createTerminalActivityIndicator() : options.terminalActivity;
|
|
388
|
-
const
|
|
402
|
+
const handler = createAdapterHandler({ ...options, terminalActivity });
|
|
403
|
+
const server = http.createServer(handler);
|
|
389
404
|
const cleanupTerminalActivity = () => {
|
|
390
405
|
if (ownsTerminalActivity) terminalActivity?.cleanup?.();
|
|
406
|
+
handler.cleanup?.();
|
|
391
407
|
};
|
|
392
408
|
server.once("close", cleanupTerminalActivity);
|
|
393
409
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createRequestAbort,
|
|
9
9
|
logRequestFailure,
|
|
10
|
+
MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
|
|
10
11
|
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
11
12
|
readJsonBody,
|
|
12
13
|
readBoundedResponseText,
|
|
@@ -60,7 +61,7 @@ function sendAnthropicUpstreamError(res, response, text) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function createAnthropicCountTokensHandler(options) {
|
|
63
|
-
const { acquireRequest, requestBodyTimeoutMs } = options;
|
|
64
|
+
const { acquireRequest, requestBodyTimeoutMs, countTokensFn = countTokens } = options;
|
|
64
65
|
|
|
65
66
|
return async function handleAnthropicCountTokens(req, res) {
|
|
66
67
|
const abort = createRequestAbort(req, res);
|
|
@@ -70,12 +71,12 @@ export function createAnthropicCountTokensHandler(options) {
|
|
|
70
71
|
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
71
72
|
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
72
73
|
abort.clearTimeout();
|
|
73
|
-
const result = await
|
|
74
|
+
const result = await countTokensFn(parsed, { signal: abort.signal });
|
|
74
75
|
releaseRequest();
|
|
75
76
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
76
77
|
res.end(JSON.stringify(result));
|
|
77
78
|
} catch (error) {
|
|
78
|
-
sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 :
|
|
79
|
+
sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 : 500);
|
|
79
80
|
} finally {
|
|
80
81
|
releaseRequest();
|
|
81
82
|
abort.cleanup();
|
|
@@ -157,7 +158,11 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
157
158
|
const upstream = await chatCompletionsFn({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
158
159
|
releaseRequest();
|
|
159
160
|
const data = upstream.ok
|
|
160
|
-
? await upstream
|
|
161
|
+
? await readBoundedResponseText(upstream, {
|
|
162
|
+
maxBytes: MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
|
|
163
|
+
label: "Copilot Chat success body",
|
|
164
|
+
signal: abort.signal,
|
|
165
|
+
})
|
|
161
166
|
: await readBoundedResponseText(upstream, {
|
|
162
167
|
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
163
168
|
label: "Copilot Chat error body",
|
package/src/anthropic.mjs
CHANGED
|
@@ -350,8 +350,7 @@ export async function streamAnthropicFromLines(lineIterator, emit, model, option
|
|
|
350
350
|
await emit("message_stop", { type: "message_stop" });
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
|
|
354
|
-
export async function countTokens(body) {
|
|
353
|
+
export function anthropicTokenText(body) {
|
|
355
354
|
const parts = [];
|
|
356
355
|
const sys = systemToText(body.system);
|
|
357
356
|
if (sys) parts.push(sys);
|
|
@@ -374,7 +373,15 @@ export async function countTokens(body) {
|
|
|
374
373
|
}
|
|
375
374
|
}
|
|
376
375
|
|
|
377
|
-
|
|
376
|
+
return parts.join("\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function countTokenText(text) {
|
|
378
380
|
const count = await tokenCounter();
|
|
379
381
|
return { input_tokens: count(text) };
|
|
380
382
|
}
|
|
383
|
+
|
|
384
|
+
// Copilot has no count_tokens endpoint; estimate locally with gpt-tokenizer.
|
|
385
|
+
export async function countTokens(body) {
|
|
386
|
+
return countTokenText(anthropicTokenText(body));
|
|
387
|
+
}
|
package/src/auth-profile.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
normalizeGithubIdentity,
|
|
13
13
|
} from "./github-identity.mjs";
|
|
14
14
|
import { withFileLock } from "./lock.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import { parseSafePositiveInteger, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
16
16
|
|
|
17
17
|
export const AUTH_PROFILE_CODEX = "codex";
|
|
18
18
|
export const AUTH_PROFILE_CLAUDE = "claude";
|
|
@@ -117,8 +117,8 @@ export function readAuthProfileCredentials(profile, { home = os.homedir() } = {}
|
|
|
117
117
|
|
|
118
118
|
function lockOptions(env = process.env) {
|
|
119
119
|
return {
|
|
120
|
-
timeoutMs:
|
|
121
|
-
staleMs:
|
|
120
|
+
timeoutMs: parseSafePositiveInteger(env.CCDX_TOKEN_LOCK_TIMEOUT_MS, RUNTIME_DEFAULTS.tokenLockTimeoutMs),
|
|
121
|
+
staleMs: parseSafePositiveInteger(env.CCDX_TOKEN_LOCK_STALE_MS, RUNTIME_DEFAULTS.tokenLockStaleMs),
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
package/src/auth.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
githubTokenFingerprint,
|
|
9
9
|
normalizeGithubIdentity,
|
|
10
10
|
} from "./github-identity.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import { parseSafePositiveInteger, parseTimerMs, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
12
12
|
import { status } from "./status.mjs";
|
|
13
13
|
import { withFileLock } from "./lock.mjs";
|
|
14
14
|
|
|
@@ -20,6 +20,7 @@ const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
|
20
20
|
const DEFAULT_DEVICE_INTERVAL_SECONDS = 5;
|
|
21
21
|
const DEFAULT_DEVICE_EXPIRES_SECONDS = 900;
|
|
22
22
|
const DEFAULT_DEVICE_CODE_TIMEOUT_MS = 30 * 1000;
|
|
23
|
+
const DEFAULT_GITHUB_IDENTITY_TIMEOUT_MS = 30 * 1000;
|
|
23
24
|
const MAX_DEVICE_FLOW_JSON_BYTES = 64 * 1024;
|
|
24
25
|
const DISABLE_TOKEN_DISCOVERY_VALUES = new Set(["1", "true", "yes"]);
|
|
25
26
|
const MAX_AUTH_JSON_BYTES = 1024 * 1024;
|
|
@@ -258,8 +259,8 @@ function isTokenDiscoveryDisabled(env = process.env) {
|
|
|
258
259
|
|
|
259
260
|
function tokenLockOptions(env = process.env) {
|
|
260
261
|
return {
|
|
261
|
-
timeoutMs:
|
|
262
|
-
staleMs:
|
|
262
|
+
timeoutMs: parseSafePositiveInteger(env.CCDX_TOKEN_LOCK_TIMEOUT_MS, RUNTIME_DEFAULTS.tokenLockTimeoutMs),
|
|
263
|
+
staleMs: parseSafePositiveInteger(env.CCDX_TOKEN_LOCK_STALE_MS, RUNTIME_DEFAULTS.tokenLockStaleMs),
|
|
263
264
|
};
|
|
264
265
|
}
|
|
265
266
|
|
|
@@ -425,19 +426,47 @@ function expectedGithubIdentity(home, env, token) {
|
|
|
425
426
|
return token ? readGithubTokenMetadata(home, token) : null;
|
|
426
427
|
}
|
|
427
428
|
|
|
428
|
-
|
|
429
|
+
function githubIdentityTimeoutError(timeoutMs) {
|
|
430
|
+
const error = new Error(`GitHub identity request timed out after ${timeoutMs}ms`);
|
|
431
|
+
error.code = "CCDX_GITHUB_IDENTITY_TIMEOUT";
|
|
432
|
+
return error;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function fetchGithubIdentity(token, {
|
|
436
|
+
fetchImpl = fetch,
|
|
437
|
+
signal,
|
|
438
|
+
timeoutMs = DEFAULT_GITHUB_IDENTITY_TIMEOUT_MS,
|
|
439
|
+
} = {}) {
|
|
429
440
|
if (typeof token !== "string" || !token.trim()) return { ok: false, reason: "empty_token" };
|
|
441
|
+
const deadlineMs = parseTimerMs(timeoutMs, DEFAULT_GITHUB_IDENTITY_TIMEOUT_MS);
|
|
442
|
+
const controller = new AbortController();
|
|
443
|
+
const onCallerAbort = () => controller.abort(signal?.reason);
|
|
444
|
+
signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
445
|
+
if (signal?.aborted) onCallerAbort();
|
|
446
|
+
const timer = setTimeout(() => controller.abort(githubIdentityTimeoutError(deadlineMs)), deadlineMs);
|
|
447
|
+
let resp;
|
|
430
448
|
try {
|
|
431
|
-
|
|
449
|
+
resp = await awaitWithSignal(() => fetchImpl(`${GITHUB_API}/user`, {
|
|
432
450
|
headers: { Authorization: `token ${token.trim()}`, Accept: "application/json" },
|
|
433
|
-
signal,
|
|
434
|
-
});
|
|
451
|
+
signal: controller.signal,
|
|
452
|
+
}), controller.signal);
|
|
435
453
|
if (!resp.ok) return { ok: false, status: resp.status, reason: "github_user_failed" };
|
|
436
|
-
const data = await resp.json();
|
|
454
|
+
const data = await awaitWithSignal(() => resp.json(), controller.signal);
|
|
437
455
|
const identity = normalizeGithubIdentity(data);
|
|
438
456
|
return identity ? { ok: true, ...identity } : { ok: false, reason: "github_identity_missing" };
|
|
439
457
|
} catch (error) {
|
|
440
|
-
|
|
458
|
+
cancelResponseBody(resp);
|
|
459
|
+
const timedOut = controller.signal.aborted && !signal?.aborted
|
|
460
|
+
&& controller.signal.reason?.code === "CCDX_GITHUB_IDENTITY_TIMEOUT";
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
transient: true,
|
|
464
|
+
reason: timedOut ? "github_user_timeout" : "github_user_request_failed",
|
|
465
|
+
error,
|
|
466
|
+
};
|
|
467
|
+
} finally {
|
|
468
|
+
clearTimeout(timer);
|
|
469
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
441
470
|
}
|
|
442
471
|
}
|
|
443
472
|
|
|
@@ -661,6 +690,7 @@ export async function ensureAuth({
|
|
|
661
690
|
log = console.log,
|
|
662
691
|
openAndCopyFn = openAndCopy,
|
|
663
692
|
deviceCodeTimeoutMs = DEFAULT_DEVICE_CODE_TIMEOUT_MS,
|
|
693
|
+
githubIdentityTimeoutMs = DEFAULT_GITHUB_IDENTITY_TIMEOUT_MS,
|
|
664
694
|
sleepImpl = sleep,
|
|
665
695
|
now = Date.now,
|
|
666
696
|
} = {}) {
|
|
@@ -720,7 +750,11 @@ export async function ensureAuth({
|
|
|
720
750
|
now,
|
|
721
751
|
});
|
|
722
752
|
if (result.state === "done") {
|
|
723
|
-
const identity = await fetchGithubIdentity(result.token, {
|
|
753
|
+
const identity = await fetchGithubIdentity(result.token, {
|
|
754
|
+
fetchImpl,
|
|
755
|
+
signal,
|
|
756
|
+
timeoutMs: githubIdentityTimeoutMs,
|
|
757
|
+
});
|
|
724
758
|
writeToken(result.token, home, identity.ok ? identity : null);
|
|
725
759
|
log(status("ok", "Login successful"));
|
|
726
760
|
return;
|
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
readUserSettings,
|
|
13
13
|
writeAutoReviewModel,
|
|
14
14
|
} from "./user-settings.mjs";
|
|
15
|
+
import { parseTimerMs } from "./runtime-config.mjs";
|
|
15
16
|
|
|
16
17
|
const DEFAULT_MODEL_FETCH_TIMEOUT_MS = 5000;
|
|
17
18
|
|
|
18
19
|
function modelFetchTimeout(env) {
|
|
19
|
-
|
|
20
|
-
return Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_MODEL_FETCH_TIMEOUT_MS;
|
|
20
|
+
return parseTimerMs(env.CCDX_MODEL_REFRESH_TIMEOUT_MS, DEFAULT_MODEL_FETCH_TIMEOUT_MS);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function sortedModelIds(modelIds, currentModel) {
|
package/src/cli-auth.mjs
CHANGED
|
@@ -15,12 +15,16 @@ import {
|
|
|
15
15
|
withAuthProfileLock,
|
|
16
16
|
writeClaudeAuthProfile,
|
|
17
17
|
} from "./auth-profile.mjs";
|
|
18
|
-
import { buildHeaders,
|
|
18
|
+
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
19
19
|
import {
|
|
20
20
|
githubIdentitiesEqual,
|
|
21
21
|
githubTokenFingerprint,
|
|
22
22
|
normalizeGithubIdentity,
|
|
23
23
|
} from "./github-identity.mjs";
|
|
24
|
+
import {
|
|
25
|
+
MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
26
|
+
readBoundedResponseText,
|
|
27
|
+
} from "./http-transport.mjs";
|
|
24
28
|
import { saveModelCache } from "./model-cache.mjs";
|
|
25
29
|
import { isClaudeCopilotModel } from "./models.mjs";
|
|
26
30
|
import { profileRouting } from "./profile-routing.mjs";
|
|
@@ -32,6 +36,8 @@ import {
|
|
|
32
36
|
terminalCell,
|
|
33
37
|
} from "./cli-table.mjs";
|
|
34
38
|
|
|
39
|
+
const DEFAULT_COPILOT_CATALOG_TIMEOUT_MS = 30 * 1000;
|
|
40
|
+
|
|
35
41
|
function openAndCopy(userCode, verificationUri) {
|
|
36
42
|
if (process.platform !== "darwin") return;
|
|
37
43
|
try {
|
|
@@ -50,6 +56,44 @@ function responseDetail(value) {
|
|
|
50
56
|
return String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
function abortError(signal) {
|
|
60
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
61
|
+
const error = new Error("The operation was aborted");
|
|
62
|
+
error.name = "AbortError";
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cancelResponseBody(response) {
|
|
67
|
+
try { Promise.resolve(response?.body?.cancel?.()).catch(() => {}); } catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fetchWithSignal(operation, signal) {
|
|
71
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let settled = false;
|
|
74
|
+
const finish = (callback, value) => {
|
|
75
|
+
if (settled) return false;
|
|
76
|
+
settled = true;
|
|
77
|
+
signal?.removeEventListener("abort", onAbort);
|
|
78
|
+
callback(value);
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
const onAbort = () => finish(reject, abortError(signal));
|
|
82
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
83
|
+
Promise.resolve()
|
|
84
|
+
.then(operation)
|
|
85
|
+
.then((response) => {
|
|
86
|
+
if (!finish(resolve, response)) cancelResponseBody(response);
|
|
87
|
+
}, (error) => finish(reject, error));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function catalogTimeoutError(label, timeoutMs) {
|
|
92
|
+
const error = new Error(`${label} model catalog timed out after ${timeoutMs}ms`);
|
|
93
|
+
error.code = "CCDX_COPILOT_CATALOG_TIMEOUT";
|
|
94
|
+
return error;
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
async function jsonResponse({ response, data, jsonError }, label) {
|
|
54
98
|
if (!response.ok) {
|
|
55
99
|
throw new Error(`${label} failed with HTTP ${response.status}`);
|
|
@@ -121,33 +165,58 @@ async function fetchCopilotCatalog(tokenData, {
|
|
|
121
165
|
signal,
|
|
122
166
|
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
123
167
|
label = "Copilot",
|
|
168
|
+
timeoutMs = DEFAULT_COPILOT_CATALOG_TIMEOUT_MS,
|
|
124
169
|
} = {}) {
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
signal,
|
|
136
|
-
});
|
|
137
|
-
const modelText = await modelResponse.text();
|
|
138
|
-
if (!modelResponse.ok) {
|
|
139
|
-
const detail = responseDetail(modelText);
|
|
140
|
-
throw new Error(`${label} model catalog failed with HTTP ${modelResponse.status}${detail ? `: ${detail}` : ""}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let catalog;
|
|
170
|
+
const parsedTimeout = Number(timeoutMs);
|
|
171
|
+
const deadlineMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0
|
|
172
|
+
? parsedTimeout
|
|
173
|
+
: DEFAULT_COPILOT_CATALOG_TIMEOUT_MS;
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const onCallerAbort = () => controller.abort(signal?.reason);
|
|
176
|
+
signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
177
|
+
if (signal?.aborted) onCallerAbort();
|
|
178
|
+
const timer = setTimeout(() => controller.abort(catalogTimeoutError(label, deadlineMs)), deadlineMs);
|
|
179
|
+
let modelResponse;
|
|
144
180
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
const apiBase = parseApiBase(tokenData);
|
|
182
|
+
const headers = buildHeaders({
|
|
183
|
+
token: tokenData.token,
|
|
184
|
+
version: vscodeVersion,
|
|
185
|
+
initiator: "user",
|
|
186
|
+
vision: false,
|
|
187
|
+
});
|
|
188
|
+
modelResponse = await fetchWithSignal(() => fetchImpl(`${apiBase}/models`, {
|
|
189
|
+
headers,
|
|
190
|
+
redirect: "error",
|
|
191
|
+
signal: controller.signal,
|
|
192
|
+
}), controller.signal);
|
|
193
|
+
const modelText = await readBoundedResponseText(modelResponse, {
|
|
194
|
+
maxBytes: MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
195
|
+
label: `${label} model catalog`,
|
|
196
|
+
signal: controller.signal,
|
|
197
|
+
});
|
|
198
|
+
if (!modelResponse.ok) {
|
|
199
|
+
const detail = responseDetail(modelText);
|
|
200
|
+
throw new Error(`${label} model catalog failed with HTTP ${modelResponse.status}${detail ? `: ${detail}` : ""}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let catalog;
|
|
204
|
+
try {
|
|
205
|
+
catalog = JSON.parse(modelText);
|
|
206
|
+
} catch {
|
|
207
|
+
throw new Error(`${label} model catalog returned invalid JSON`);
|
|
208
|
+
}
|
|
209
|
+
catalogModels(catalog);
|
|
210
|
+
return { catalog, apiBase };
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (signal?.aborted) throw abortError(signal);
|
|
213
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
214
|
+
throw error;
|
|
215
|
+
} finally {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
218
|
+
if (controller.signal.aborted) cancelResponseBody(modelResponse);
|
|
148
219
|
}
|
|
149
|
-
catalogModels(catalog);
|
|
150
|
-
return { catalog, apiBase };
|
|
151
220
|
}
|
|
152
221
|
|
|
153
222
|
export async function validateClaudeCandidate(githubToken, {
|
|
@@ -156,6 +225,7 @@ export async function validateClaudeCandidate(githubToken, {
|
|
|
156
225
|
fetchImpl = fetch,
|
|
157
226
|
signal,
|
|
158
227
|
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
228
|
+
catalogTimeoutMs,
|
|
159
229
|
} = {}) {
|
|
160
230
|
const validation = await validateGithubToken(githubToken, { fetchImpl, signal });
|
|
161
231
|
if (!validation.ok) {
|
|
@@ -180,6 +250,7 @@ export async function validateClaudeCandidate(githubToken, {
|
|
|
180
250
|
signal,
|
|
181
251
|
vscodeVersion,
|
|
182
252
|
label: "Claude account",
|
|
253
|
+
timeoutMs: catalogTimeoutMs,
|
|
183
254
|
});
|
|
184
255
|
const models = claudeModels(catalog);
|
|
185
256
|
if (!models.length) {
|
|
@@ -205,6 +276,7 @@ export async function authorizeClaudeProfile({
|
|
|
205
276
|
log = console.log,
|
|
206
277
|
openAndCopyFn = openAndCopy,
|
|
207
278
|
deviceCodeTimeoutMs,
|
|
279
|
+
catalogTimeoutMs,
|
|
208
280
|
sleepImpl,
|
|
209
281
|
now = Date.now,
|
|
210
282
|
saveModelCacheFn = saveModelCache,
|
|
@@ -268,6 +340,7 @@ export async function authorizeClaudeProfile({
|
|
|
268
340
|
expectedLogin,
|
|
269
341
|
fetchImpl,
|
|
270
342
|
signal,
|
|
343
|
+
catalogTimeoutMs,
|
|
271
344
|
});
|
|
272
345
|
await withAuthProfileLock(AUTH_PROFILE_CODEX, () => {
|
|
273
346
|
const currentCodex = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { terminalCell } from "./cli-table.mjs";
|
|
2
|
+
import { status } from "./status.mjs";
|
|
3
|
+
|
|
4
|
+
const MAX_MESSAGE_CHARACTERS = 1024;
|
|
5
|
+
const MAX_DETAIL_CHARACTERS = 512;
|
|
6
|
+
const MAX_DETAILS = 16;
|
|
7
|
+
|
|
8
|
+
function redactSensitiveValues(value) {
|
|
9
|
+
return value
|
|
10
|
+
.replace(/\b(authorization\s*[:=]\s*)(?:bearer\s+)?[^\s,;]+/gi, "$1[REDACTED]")
|
|
11
|
+
.replace(/\bbearer\s+[^\s,;]+/gi, "Bearer [REDACTED]")
|
|
12
|
+
.replace(/\b((?:api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|password|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[REDACTED]")
|
|
13
|
+
.replace(/\b(?:github_pat_[A-Za-z0-9_]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}|sk-[A-Za-z0-9_-]{8,})\b/g, "[REDACTED]");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function boundedTerminalText(value, fallback, maxCharacters) {
|
|
17
|
+
try {
|
|
18
|
+
return redactSensitiveValues(terminalCell(value, { fallback })).slice(0, maxCharacters);
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatCliError(error) {
|
|
25
|
+
let rawMessage;
|
|
26
|
+
try {
|
|
27
|
+
rawMessage = error?.message;
|
|
28
|
+
} catch {
|
|
29
|
+
rawMessage = undefined;
|
|
30
|
+
}
|
|
31
|
+
const message = boundedTerminalText(rawMessage, "Command failed", MAX_MESSAGE_CHARACTERS);
|
|
32
|
+
let details;
|
|
33
|
+
try {
|
|
34
|
+
details = error?.details;
|
|
35
|
+
} catch {
|
|
36
|
+
details = undefined;
|
|
37
|
+
}
|
|
38
|
+
const values = details === undefined
|
|
39
|
+
? []
|
|
40
|
+
: Array.isArray(details) ? details : [details];
|
|
41
|
+
const visible = values.slice(0, MAX_DETAILS);
|
|
42
|
+
const lines = [status("err", message)];
|
|
43
|
+
for (const detail of visible) {
|
|
44
|
+
lines.push(status("info", boundedTerminalText(detail, "Detail unavailable", MAX_DETAIL_CHARACTERS)));
|
|
45
|
+
}
|
|
46
|
+
if (values.length > visible.length) {
|
|
47
|
+
lines.push(status("info", `${values.length - visible.length} additional detail(s) omitted`));
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
package/src/cli-models.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
AUTH_PROFILE_CODEX,
|
|
6
6
|
readAuthProfileCredentials,
|
|
7
7
|
} from "./auth-profile.mjs";
|
|
8
|
-
import { buildHeaders,
|
|
8
|
+
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
9
9
|
import {
|
|
10
10
|
isClaudeCopilotCatalogEntry,
|
|
11
11
|
isClaudeCopilotModel,
|
|
@@ -134,7 +134,7 @@ export async function fetchLiveCopilotModels({
|
|
|
134
134
|
if (!validation.ok) throw new Error(validationFailure(validation));
|
|
135
135
|
|
|
136
136
|
const tokenData = validation.copilotTokenData;
|
|
137
|
-
const apiBase = tokenData
|
|
137
|
+
const apiBase = parseApiBase(tokenData);
|
|
138
138
|
const headers = buildHeaders({
|
|
139
139
|
token: tokenData.token,
|
|
140
140
|
version: vscodeVersion,
|
package/src/cli-options.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MAX_TIMER_DELAY_MS } from "./runtime-config.mjs";
|
|
2
|
+
|
|
1
3
|
const HELP_COMMANDS = new Set(["help", "--help", "-h"]);
|
|
2
4
|
const VERSION_COMMANDS = new Set(["version", "--version", "-v"]);
|
|
3
5
|
const START_OPTIONS = new Map([
|
|
@@ -262,13 +264,13 @@ export function parseRuntimeOptions(env = process.env) {
|
|
|
262
264
|
return {
|
|
263
265
|
adapterPort: integerEnv(env, "ADAPTER_PORT", 2026, { min: 1, max: 65535 }),
|
|
264
266
|
adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
|
|
265
|
-
modelRefreshTimeoutMs: integerEnv(env, "CCDX_MODEL_REFRESH_TIMEOUT_MS", 5000, { min: 1 }),
|
|
266
|
-
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1 }),
|
|
267
|
-
modelRefreshIntervalMs: integerEnv(env, "CCDX_MODEL_REFRESH_INTERVAL_MS", 2 * 60 * 60 * 1000, { min: 0 }),
|
|
268
|
-
upstreamTimeoutMs: integerEnv(env, "CCDX_UPSTREAM_TIMEOUT_MS", 120000, { min: 1 }),
|
|
269
|
-
streamHandshakeTimeoutMs: integerEnv(env, "CCDX_STREAM_HANDSHAKE_TIMEOUT_MS", 120000, { min: 1 }),
|
|
270
|
-
streamIdleTimeoutMs: integerEnv(env, "CCDX_STREAM_IDLE_TIMEOUT_MS", 120000, { min: 1 }),
|
|
271
|
-
shutdownTimeoutMs: integerEnv(env, "CCDX_SHUTDOWN_TIMEOUT_MS", 5000, { min: 1 }),
|
|
267
|
+
modelRefreshTimeoutMs: integerEnv(env, "CCDX_MODEL_REFRESH_TIMEOUT_MS", 5000, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
268
|
+
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
269
|
+
modelRefreshIntervalMs: integerEnv(env, "CCDX_MODEL_REFRESH_INTERVAL_MS", 2 * 60 * 60 * 1000, { min: 0, max: MAX_TIMER_DELAY_MS }),
|
|
270
|
+
upstreamTimeoutMs: integerEnv(env, "CCDX_UPSTREAM_TIMEOUT_MS", 120000, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
271
|
+
streamHandshakeTimeoutMs: integerEnv(env, "CCDX_STREAM_HANDSHAKE_TIMEOUT_MS", 120000, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
272
|
+
streamIdleTimeoutMs: integerEnv(env, "CCDX_STREAM_IDLE_TIMEOUT_MS", 120000, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
273
|
+
shutdownTimeoutMs: integerEnv(env, "CCDX_SHUTDOWN_TIMEOUT_MS", 5000, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
272
274
|
};
|
|
273
275
|
}
|
|
274
276
|
|
|
@@ -276,7 +278,7 @@ export function parseAdapterProbeOptions(env = process.env) {
|
|
|
276
278
|
return {
|
|
277
279
|
adapterPort: integerEnv(env, "ADAPTER_PORT", 2026, { min: 1, max: 65535 }),
|
|
278
280
|
adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
|
|
279
|
-
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1 }),
|
|
281
|
+
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
280
282
|
};
|
|
281
283
|
}
|
|
282
284
|
|
|
@@ -60,6 +60,20 @@ function refreshError(message, { statusCode, transient = false } = {}) {
|
|
|
60
60
|
return error;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function transientRefreshError(error) {
|
|
64
|
+
let message = "Copilot token request failed";
|
|
65
|
+
try {
|
|
66
|
+
if (typeof error?.message === "string" && error.message) message = error.message;
|
|
67
|
+
} catch {}
|
|
68
|
+
const wrapped = new Error(message, { cause: error });
|
|
69
|
+
try {
|
|
70
|
+
if (typeof error?.code === "string") wrapped.code = error.code;
|
|
71
|
+
if (Number.isInteger(error?.statusCode)) wrapped.statusCode = error.statusCode;
|
|
72
|
+
} catch {}
|
|
73
|
+
wrapped.transient = true;
|
|
74
|
+
return wrapped;
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
function copilotProfileLabel(profile) {
|
|
64
78
|
if (profile === "codex") return "Copilot";
|
|
65
79
|
const name = String(profile || "profile");
|
|
@@ -189,6 +203,7 @@ export function createCopilotTokenSession({
|
|
|
189
203
|
allowAccountSwitch = false,
|
|
190
204
|
}) {
|
|
191
205
|
if (!data.token) throw new Error("Copilot token response missing token field");
|
|
206
|
+
const nextApiBase = parseApiBase(data);
|
|
192
207
|
const fingerprint = githubTokenFingerprint(githubToken);
|
|
193
208
|
const normalizedIdentity = normalizeGithubIdentity(identity);
|
|
194
209
|
const tokenChanged = Boolean(githubFingerprint && fingerprint !== githubFingerprint);
|
|
@@ -204,7 +219,7 @@ export function createCopilotTokenSession({
|
|
|
204
219
|
throw error;
|
|
205
220
|
}
|
|
206
221
|
copilotToken = data.token;
|
|
207
|
-
apiBase =
|
|
222
|
+
apiBase = nextApiBase;
|
|
208
223
|
copilotTokenExpiry = typeof data.expires_at === "number"
|
|
209
224
|
? data.expires_at * 1000
|
|
210
225
|
: Date.now() + 25 * 60 * 1000;
|
|
@@ -232,7 +247,7 @@ export function createCopilotTokenSession({
|
|
|
232
247
|
signal,
|
|
233
248
|
}, { fetchImpl, ...tokenRetryOptions });
|
|
234
249
|
} catch (error) {
|
|
235
|
-
if (!isAbortError(error, signal)) error
|
|
250
|
+
if (!isAbortError(error, signal)) throw transientRefreshError(error);
|
|
236
251
|
throw error;
|
|
237
252
|
}
|
|
238
253
|
if (!response.ok) {
|