codex-copilot-dx 0.5.6 → 0.6.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 +126 -5
- package/bin/cli.mjs +74 -92
- package/package.json +1 -1
- package/src/adapter.mjs +72 -14
- package/src/auth-profile.mjs +205 -0
- package/src/cli-auth.mjs +493 -0
- package/src/cli-models.mjs +38 -13
- package/src/cli-options.mjs +100 -12
- package/src/cli-status.mjs +29 -3
- package/src/copilot-client.mjs +226 -0
- package/src/copilot-token-session.mjs +362 -0
- package/src/copilot.mjs +45 -387
- package/src/doctor.mjs +169 -23
- package/src/model-cache.mjs +22 -6
- package/src/observability.mjs +61 -3
- package/src/pm-studio-asar.mjs +392 -0
- package/src/pm-studio-relay.mjs +557 -0
- package/src/pm-studio-setup.mjs +734 -0
- package/src/profile-model-runtime.mjs +261 -0
- package/src/profile-runtime.mjs +80 -0
- package/src/responses-handler.mjs +7 -6
- package/src/running-adapter.mjs +16 -0
package/src/cli-models.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import os from "node:os";
|
|
3
|
-
import {
|
|
2
|
+
import { validateGithubToken } from "./auth.mjs";
|
|
3
|
+
import {
|
|
4
|
+
AUTH_PROFILE_CLAUDE,
|
|
5
|
+
AUTH_PROFILE_CODEX,
|
|
6
|
+
readAuthProfileCredentials,
|
|
7
|
+
} from "./auth-profile.mjs";
|
|
4
8
|
import { buildHeaders, DEFAULT_API_BASE, FALLBACK_VSCODE_VERSION } from "./copilot.mjs";
|
|
5
9
|
import { status } from "./status.mjs";
|
|
6
10
|
|
|
@@ -69,21 +73,41 @@ async function responseText(response) {
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
function readModelProfile(profile, home) {
|
|
77
|
+
const requested = String(profile || AUTH_PROFILE_CODEX).trim().toLowerCase();
|
|
78
|
+
if (requested !== AUTH_PROFILE_CODEX && requested !== AUTH_PROFILE_CLAUDE) {
|
|
79
|
+
throw new Error(`Unsupported models profile: ${profile}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let credentials;
|
|
83
|
+
try {
|
|
84
|
+
credentials = readAuthProfileCredentials(requested, { home });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const label = requested === AUTH_PROFILE_CLAUDE ? "Claude GitHub profile" : "GitHub token";
|
|
87
|
+
throw new Error(`Could not read the ${label}: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
if (credentials.valid) return credentials;
|
|
90
|
+
|
|
91
|
+
if (requested === AUTH_PROFILE_CLAUDE) {
|
|
92
|
+
if (!credentials.configured) {
|
|
93
|
+
throw new Error("Claude GitHub profile is not configured. Run ccdx auth login claude.");
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Claude GitHub profile is invalid (${credentials.reason || "unknown"}). Run ccdx auth login claude --reauth.`);
|
|
96
|
+
}
|
|
97
|
+
if (!credentials.configured) throw new Error("GitHub token not found. Start ccdx once to log in.");
|
|
98
|
+
if (credentials.reason === "empty_token") throw new Error("GitHub token file is empty. Start ccdx again to log in.");
|
|
99
|
+
throw new Error(`GitHub token is unavailable (${credentials.reason || "unknown"}). Start ccdx again to log in.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
export async function fetchLiveCopilotModels({
|
|
73
103
|
home = os.homedir(),
|
|
104
|
+
profile = AUTH_PROFILE_CODEX,
|
|
74
105
|
fetchImpl = fetch,
|
|
75
106
|
timeoutMs = DEFAULT_MODELS_TIMEOUT_MS,
|
|
76
107
|
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
77
108
|
} = {}) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
githubToken = fs.readFileSync(tokenPath, "utf8").trim();
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (error?.code === "ENOENT") throw new Error("GitHub token not found. Start ccdx once to log in.");
|
|
84
|
-
throw new Error(`Could not read the GitHub token: ${error.message}`);
|
|
85
|
-
}
|
|
86
|
-
if (!githubToken) throw new Error("GitHub token file is empty. Start ccdx again to log in.");
|
|
109
|
+
const credentials = readModelProfile(profile, home);
|
|
110
|
+
const githubToken = credentials.token;
|
|
87
111
|
|
|
88
112
|
const controller = new AbortController();
|
|
89
113
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -123,7 +147,7 @@ export async function fetchLiveCopilotModels({
|
|
|
123
147
|
const catalog = selectableCopilotModels(payload);
|
|
124
148
|
let upstreamHost = "GitHub Copilot";
|
|
125
149
|
try { upstreamHost = new URL(apiBase).hostname; } catch {}
|
|
126
|
-
return { ...catalog, upstreamHost: safeText(upstreamHost, "GitHub Copilot") };
|
|
150
|
+
return { ...catalog, profile: credentials.profile, upstreamHost: safeText(upstreamHost, "GitHub Copilot") };
|
|
127
151
|
} catch (error) {
|
|
128
152
|
if (controller.signal.aborted && !/timed out/.test(error?.message || "")) {
|
|
129
153
|
throw new Error(`Live model lookup timed out after ${timeoutMs}ms`);
|
|
@@ -136,12 +160,13 @@ export async function fetchLiveCopilotModels({
|
|
|
136
160
|
|
|
137
161
|
export function formatLiveCopilotModels(catalog, { commandName = "ccdx" } = {}) {
|
|
138
162
|
const models = Array.isArray(catalog?.models) ? catalog.models : [];
|
|
163
|
+
const profile = catalog?.profile === AUTH_PROFILE_CLAUDE ? AUTH_PROFILE_CLAUDE : AUTH_PROFILE_CODEX;
|
|
139
164
|
const responses = models.filter((model) => model.endpoints.includes("responses")).length;
|
|
140
165
|
const chat = models.filter((model) => model.endpoints.includes("chat")).length;
|
|
141
166
|
const claude = models.filter((model) => model.id.toLowerCase().startsWith("claude-")
|
|
142
167
|
|| model.vendor.toLowerCase() === "anthropic").length;
|
|
143
168
|
const lines = [
|
|
144
|
-
`${commandName} models`,
|
|
169
|
+
`${commandName} models${profile === AUTH_PROFILE_CLAUDE ? " --profile claude" : ""}`,
|
|
145
170
|
status("ok", `Live catalog from ${safeText(catalog?.upstreamHost, "GitHub Copilot")}: ${models.length} selectable of ${Number(catalog?.advertised) || 0} advertised`),
|
|
146
171
|
status("info", `Responses: ${responses}; Chat: ${chat}; Claude/Anthropic: ${claude}`),
|
|
147
172
|
];
|
package/src/cli-options.mjs
CHANGED
|
@@ -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,9 +58,62 @@ 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 === "pms") {
|
|
62
|
+
const [action, ...pmsArgs] = rest;
|
|
63
|
+
if (!action) throw new Error("Missing pms action: expected setup");
|
|
64
|
+
if (action !== "setup") throw new Error(`Unknown pms action: ${action}`);
|
|
65
|
+
if (pmsArgs.length) unexpectedArgs(pmsArgs);
|
|
66
|
+
return { command: "pms", action: "setup", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
67
|
+
}
|
|
29
68
|
if (command === "models") {
|
|
30
|
-
|
|
31
|
-
return {
|
|
69
|
+
const options = parseOptions(rest, new Map([["--profile", "value"]]));
|
|
70
|
+
return {
|
|
71
|
+
command: "models",
|
|
72
|
+
configureClaudeDesktop: false,
|
|
73
|
+
showRequestId: false,
|
|
74
|
+
online: false,
|
|
75
|
+
compat: false,
|
|
76
|
+
profile: checkedProfile(options["--profile"]),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (command === "auth") {
|
|
80
|
+
const [action, ...authArgs] = rest;
|
|
81
|
+
if (!action) throw new Error("Missing auth action: expected status or login");
|
|
82
|
+
if (action === "status") {
|
|
83
|
+
const options = parseOptions(authArgs, new Map([["--online", "flag"]]));
|
|
84
|
+
return {
|
|
85
|
+
command: "auth",
|
|
86
|
+
action: "status",
|
|
87
|
+
profile: "",
|
|
88
|
+
configureClaudeDesktop: false,
|
|
89
|
+
showRequestId: false,
|
|
90
|
+
online: Boolean(options["--online"]),
|
|
91
|
+
compat: false,
|
|
92
|
+
expectedLogin: "",
|
|
93
|
+
reauth: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (action === "login") {
|
|
97
|
+
const [profile, ...loginArgs] = authArgs;
|
|
98
|
+
if (!profile) throw new Error("Missing auth login profile: expected claude");
|
|
99
|
+
if (profile !== "claude") throw new Error(`Auth login profile must be claude: ${profile}`);
|
|
100
|
+
const options = parseOptions(loginArgs, new Map([
|
|
101
|
+
["--github-login", "value"],
|
|
102
|
+
["--reauth", "flag"],
|
|
103
|
+
]));
|
|
104
|
+
return {
|
|
105
|
+
command: "auth",
|
|
106
|
+
action: "login",
|
|
107
|
+
profile: "claude",
|
|
108
|
+
configureClaudeDesktop: false,
|
|
109
|
+
showRequestId: false,
|
|
110
|
+
online: false,
|
|
111
|
+
compat: false,
|
|
112
|
+
expectedLogin: options["--github-login"] || "",
|
|
113
|
+
reauth: Boolean(options["--reauth"]),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unknown auth action: ${action}`);
|
|
32
117
|
}
|
|
33
118
|
if (command === "auto-review-model") {
|
|
34
119
|
if (rest.length) unexpectedArgs(rest);
|
|
@@ -50,18 +135,18 @@ export function parseCliArgs(args = []) {
|
|
|
50
135
|
};
|
|
51
136
|
}
|
|
52
137
|
if (command === "doctor" || command === "--doctor") {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
138
|
+
const options = parseOptions(rest, new Map([
|
|
139
|
+
["--online", "flag"],
|
|
140
|
+
["--compat", "flag"],
|
|
141
|
+
["--profile", "value"],
|
|
142
|
+
]));
|
|
59
143
|
return {
|
|
60
144
|
command: "doctor",
|
|
61
145
|
configureClaudeDesktop: false,
|
|
62
146
|
showRequestId: false,
|
|
63
|
-
online:
|
|
64
|
-
compat:
|
|
147
|
+
online: Boolean(options["--online"]),
|
|
148
|
+
compat: Boolean(options["--compat"]),
|
|
149
|
+
profile: checkedProfile(options["--profile"], { allowAll: true }),
|
|
65
150
|
};
|
|
66
151
|
}
|
|
67
152
|
if (START_OPTIONS.has(command)) {
|
|
@@ -127,9 +212,12 @@ export function cliHelp(commandName = "ccdx") {
|
|
|
127
212
|
const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
|
|
128
213
|
return `Usage:
|
|
129
214
|
${name} [--configure-claude-desktop] [--show-request-id]
|
|
130
|
-
${name}
|
|
215
|
+
${name} auth status [--online]
|
|
216
|
+
${name} auth login claude [--github-login <login>] [--reauth]
|
|
217
|
+
${name} doctor [--online] [--compat] [--profile codex|claude|all]
|
|
131
218
|
${name} status
|
|
132
|
-
${name} models
|
|
219
|
+
${name} models [--profile codex|claude]
|
|
220
|
+
${name} pms setup
|
|
133
221
|
${name} usage
|
|
134
222
|
${name} auto-review-model
|
|
135
223
|
${name} update [npm|github]
|
package/src/cli-status.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
+
}
|