codex-copilot-dx 0.9.17 → 0.9.18
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/package.json +1 -1
- package/src/adapter.mjs +3 -4
- package/src/auth-status.mjs +175 -0
- package/src/cli-auth.mjs +2 -180
- package/src/cli-models.mjs +1 -131
- package/src/cli-table.mjs +3 -11
- package/src/copilot-responses-compat.mjs +1 -1
- package/src/dashboard-api.mjs +4 -4
- package/src/dashboard.mjs +1 -1
- package/src/http-errors.mjs +5 -0
- package/src/http-transport.mjs +6 -350
- package/src/image-mcp.mjs +1 -1
- package/src/live-models.mjs +137 -0
- package/src/observability.mjs +2 -22
- package/src/request-admission.mjs +348 -0
- package/src/response-failures.mjs +1 -1
- package/src/response-history.mjs +1 -1
- package/src/responses-compaction.mjs +1 -1
- package/src/responses-handler.mjs +1 -1
- package/src/responses-proxy.mjs +2 -2
- package/src/security.mjs +22 -0
- package/src/terminal-text.mjs +11 -0
- package/src/usage-analytics.mjs +1 -1
- package/src/usage-store.mjs +369 -0
- package/src/usage.mjs +17 -368
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
ADAPTER_STATUS_PATH,
|
|
7
7
|
classifyAdapterRoute,
|
|
8
8
|
createRequestMetrics,
|
|
9
|
-
isLoopbackAddress,
|
|
10
|
-
isLoopbackHostHeader,
|
|
11
9
|
runtimeStatusPayload,
|
|
12
10
|
} from "./observability.mjs";
|
|
13
11
|
import { createRequestId, runWithRequestContext } from "./request-context.mjs";
|
|
@@ -26,13 +24,14 @@ import {
|
|
|
26
24
|
} from "./cache-control.mjs";
|
|
27
25
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
28
26
|
import {
|
|
29
|
-
createRequestAdmission,
|
|
30
27
|
createRequestAbort,
|
|
31
|
-
httpError,
|
|
32
28
|
logRequestFailure,
|
|
33
29
|
readJsonBody,
|
|
34
30
|
sendJsonError,
|
|
35
31
|
} from "./http-transport.mjs";
|
|
32
|
+
import { httpError } from "./http-errors.mjs";
|
|
33
|
+
import { createRequestAdmission } from "./request-admission.mjs";
|
|
34
|
+
import { isLoopbackAddress, isLoopbackHostHeader } from "./security.mjs";
|
|
36
35
|
|
|
37
36
|
export {
|
|
38
37
|
abortErrorStatusCode,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { validateGithubToken } from "./auth.mjs";
|
|
3
|
+
import { AUTH_PROFILE_CODEX, readAuthProfileCredentials } from "./auth-profile.mjs";
|
|
4
|
+
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
5
|
+
import { MAX_UPSTREAM_MODEL_CATALOG_BYTES, readBoundedResponseText } from "./http-transport.mjs";
|
|
6
|
+
import { profileRouting } from "./profile-routing.mjs";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_COPILOT_CATALOG_TIMEOUT_MS = 30 * 1000;
|
|
9
|
+
|
|
10
|
+
function responseDetail(value) {
|
|
11
|
+
return String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function abortError(signal) {
|
|
15
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
16
|
+
const error = new Error("The operation was aborted");
|
|
17
|
+
error.name = "AbortError";
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function catalogModels(payload) {
|
|
22
|
+
const models = Array.isArray(payload) ? payload : payload?.data;
|
|
23
|
+
if (!Array.isArray(models)) throw new Error("Copilot model catalog contained no model list");
|
|
24
|
+
return models;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchCopilotCatalog(tokenData, {
|
|
28
|
+
fetchImpl,
|
|
29
|
+
signal,
|
|
30
|
+
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
31
|
+
timeoutMs = DEFAULT_COPILOT_CATALOG_TIMEOUT_MS,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const parsedTimeout = Number(timeoutMs);
|
|
34
|
+
const deadlineMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0
|
|
35
|
+
? parsedTimeout
|
|
36
|
+
: DEFAULT_COPILOT_CATALOG_TIMEOUT_MS;
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const onCallerAbort = () => controller.abort(signal?.reason);
|
|
39
|
+
signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
40
|
+
if (signal?.aborted) onCallerAbort();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(new Error(`Copilot model catalog timed out after ${deadlineMs}ms`)), deadlineMs);
|
|
42
|
+
try {
|
|
43
|
+
const apiBase = parseApiBase(tokenData);
|
|
44
|
+
const headers = buildHeaders({
|
|
45
|
+
token: tokenData.token,
|
|
46
|
+
version: vscodeVersion,
|
|
47
|
+
initiator: "user",
|
|
48
|
+
vision: false,
|
|
49
|
+
});
|
|
50
|
+
const response = await fetchImpl(`${apiBase}/models`, {
|
|
51
|
+
headers,
|
|
52
|
+
redirect: "error",
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
const modelText = await readBoundedResponseText(response, {
|
|
56
|
+
maxBytes: MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
57
|
+
label: "Copilot model catalog",
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const detail = responseDetail(modelText);
|
|
62
|
+
throw new Error(`Copilot model catalog failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let catalog;
|
|
66
|
+
try {
|
|
67
|
+
catalog = JSON.parse(modelText);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error("Copilot model catalog returned invalid JSON");
|
|
70
|
+
}
|
|
71
|
+
catalogModels(catalog);
|
|
72
|
+
return { catalog, apiBase };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (signal?.aborted) throw abortError(signal);
|
|
75
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
76
|
+
throw error;
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function publicProfile(credentials) {
|
|
84
|
+
return {
|
|
85
|
+
configured: credentials.configured,
|
|
86
|
+
valid: credentials.valid,
|
|
87
|
+
reason: credentials.reason,
|
|
88
|
+
login: credentials.identity?.login || "",
|
|
89
|
+
id: credentials.identity?.id || "",
|
|
90
|
+
source: "legacy",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readStatusCredentials(home) {
|
|
95
|
+
try {
|
|
96
|
+
return readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
97
|
+
} catch {
|
|
98
|
+
return {
|
|
99
|
+
profile: AUTH_PROFILE_CODEX,
|
|
100
|
+
configured: true,
|
|
101
|
+
valid: false,
|
|
102
|
+
reason: "credential_read_failed",
|
|
103
|
+
token: "",
|
|
104
|
+
identity: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function authStatus({ home = os.homedir() } = {}) {
|
|
110
|
+
const codex = readStatusCredentials(home);
|
|
111
|
+
return {
|
|
112
|
+
profiles: { codex: publicProfile(codex) },
|
|
113
|
+
routing: profileRouting(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function inspectProfileOnline(credentials, { fetchImpl, timeoutMs } = {}) {
|
|
118
|
+
if (!credentials.valid) {
|
|
119
|
+
return { checked: false, ok: false, reason: credentials.reason || "unconfigured" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timer = setTimeout(() => controller.abort(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
124
|
+
try {
|
|
125
|
+
const validation = await validateGithubToken(credentials.token, {
|
|
126
|
+
fetchImpl,
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
});
|
|
129
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
130
|
+
if (!validation.ok) {
|
|
131
|
+
return {
|
|
132
|
+
checked: true,
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: validation.reason,
|
|
135
|
+
httpStatus: validation.status || null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const { catalog, apiBase } = await fetchCopilotCatalog(validation.copilotTokenData, {
|
|
139
|
+
fetchImpl,
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
});
|
|
142
|
+
let upstreamHost = "GitHub Copilot";
|
|
143
|
+
try { upstreamHost = new URL(apiBase).hostname; } catch {}
|
|
144
|
+
return {
|
|
145
|
+
checked: true,
|
|
146
|
+
ok: true,
|
|
147
|
+
login: validation.login || credentials.identity?.login || "",
|
|
148
|
+
models: catalogModels(catalog).length,
|
|
149
|
+
upstreamHost,
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
checked: true,
|
|
154
|
+
ok: false,
|
|
155
|
+
reason: controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error.message,
|
|
156
|
+
};
|
|
157
|
+
} finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function authStatusOnline({
|
|
163
|
+
home = os.homedir(),
|
|
164
|
+
fetchImpl = fetch,
|
|
165
|
+
timeoutMs = 10000,
|
|
166
|
+
} = {}) {
|
|
167
|
+
const local = authStatus({ home });
|
|
168
|
+
const online = await inspectProfileOnline(readStatusCredentials(home), { fetchImpl, timeoutMs });
|
|
169
|
+
return {
|
|
170
|
+
...local,
|
|
171
|
+
profiles: {
|
|
172
|
+
codex: { ...local.profiles.codex, online },
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
package/src/cli-auth.mjs
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { validateGithubToken } from "./auth.mjs";
|
|
3
|
-
import {
|
|
4
|
-
AUTH_PROFILE_CODEX,
|
|
5
|
-
readAuthProfileCredentials,
|
|
6
|
-
} from "./auth-profile.mjs";
|
|
7
|
-
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
8
|
-
import {
|
|
9
|
-
MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
10
|
-
readBoundedResponseText,
|
|
11
|
-
} from "./http-transport.mjs";
|
|
12
|
-
import { profileRouting } from "./profile-routing.mjs";
|
|
1
|
+
import { authStatus, authStatusOnline } from "./auth-status.mjs";
|
|
13
2
|
import { status } from "./status.mjs";
|
|
14
3
|
import {
|
|
15
4
|
cliOutputFormat,
|
|
@@ -18,174 +7,7 @@ import {
|
|
|
18
7
|
terminalCell,
|
|
19
8
|
} from "./cli-table.mjs";
|
|
20
9
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function responseDetail(value) {
|
|
24
|
-
return String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function abortError(signal) {
|
|
28
|
-
if (signal?.reason instanceof Error) return signal.reason;
|
|
29
|
-
const error = new Error("The operation was aborted");
|
|
30
|
-
error.name = "AbortError";
|
|
31
|
-
return error;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function catalogModels(payload) {
|
|
35
|
-
const models = Array.isArray(payload) ? payload : payload?.data;
|
|
36
|
-
if (!Array.isArray(models)) throw new Error("Copilot model catalog contained no model list");
|
|
37
|
-
return models;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function fetchCopilotCatalog(tokenData, {
|
|
41
|
-
fetchImpl,
|
|
42
|
-
signal,
|
|
43
|
-
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
44
|
-
timeoutMs = DEFAULT_COPILOT_CATALOG_TIMEOUT_MS,
|
|
45
|
-
} = {}) {
|
|
46
|
-
const parsedTimeout = Number(timeoutMs);
|
|
47
|
-
const deadlineMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0
|
|
48
|
-
? parsedTimeout
|
|
49
|
-
: DEFAULT_COPILOT_CATALOG_TIMEOUT_MS;
|
|
50
|
-
const controller = new AbortController();
|
|
51
|
-
const onCallerAbort = () => controller.abort(signal?.reason);
|
|
52
|
-
signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
53
|
-
if (signal?.aborted) onCallerAbort();
|
|
54
|
-
const timer = setTimeout(() => controller.abort(new Error(`Copilot model catalog timed out after ${deadlineMs}ms`)), deadlineMs);
|
|
55
|
-
try {
|
|
56
|
-
const apiBase = parseApiBase(tokenData);
|
|
57
|
-
const headers = buildHeaders({
|
|
58
|
-
token: tokenData.token,
|
|
59
|
-
version: vscodeVersion,
|
|
60
|
-
initiator: "user",
|
|
61
|
-
vision: false,
|
|
62
|
-
});
|
|
63
|
-
const response = await fetchImpl(`${apiBase}/models`, {
|
|
64
|
-
headers,
|
|
65
|
-
redirect: "error",
|
|
66
|
-
signal: controller.signal,
|
|
67
|
-
});
|
|
68
|
-
const modelText = await readBoundedResponseText(response, {
|
|
69
|
-
maxBytes: MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
70
|
-
label: "Copilot model catalog",
|
|
71
|
-
signal: controller.signal,
|
|
72
|
-
});
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
const detail = responseDetail(modelText);
|
|
75
|
-
throw new Error(`Copilot model catalog failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let catalog;
|
|
79
|
-
try {
|
|
80
|
-
catalog = JSON.parse(modelText);
|
|
81
|
-
} catch {
|
|
82
|
-
throw new Error("Copilot model catalog returned invalid JSON");
|
|
83
|
-
}
|
|
84
|
-
catalogModels(catalog);
|
|
85
|
-
return { catalog, apiBase };
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (signal?.aborted) throw abortError(signal);
|
|
88
|
-
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
89
|
-
throw error;
|
|
90
|
-
} finally {
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
signal?.removeEventListener("abort", onCallerAbort);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function publicProfile(credentials) {
|
|
97
|
-
return {
|
|
98
|
-
configured: credentials.configured,
|
|
99
|
-
valid: credentials.valid,
|
|
100
|
-
reason: credentials.reason,
|
|
101
|
-
login: credentials.identity?.login || "",
|
|
102
|
-
id: credentials.identity?.id || "",
|
|
103
|
-
source: "legacy",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function readStatusCredentials(home) {
|
|
108
|
-
try {
|
|
109
|
-
return readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
110
|
-
} catch {
|
|
111
|
-
return {
|
|
112
|
-
profile: AUTH_PROFILE_CODEX,
|
|
113
|
-
configured: true,
|
|
114
|
-
valid: false,
|
|
115
|
-
reason: "credential_read_failed",
|
|
116
|
-
token: "",
|
|
117
|
-
identity: null,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function authStatus({ home = os.homedir() } = {}) {
|
|
123
|
-
const codex = readStatusCredentials(home);
|
|
124
|
-
return {
|
|
125
|
-
profiles: { codex: publicProfile(codex) },
|
|
126
|
-
routing: profileRouting(),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function inspectProfileOnline(credentials, { fetchImpl, timeoutMs } = {}) {
|
|
131
|
-
if (!credentials.valid) {
|
|
132
|
-
return { checked: false, ok: false, reason: credentials.reason || "unconfigured" };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const controller = new AbortController();
|
|
136
|
-
const timer = setTimeout(() => controller.abort(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
137
|
-
try {
|
|
138
|
-
const validation = await validateGithubToken(credentials.token, {
|
|
139
|
-
fetchImpl,
|
|
140
|
-
signal: controller.signal,
|
|
141
|
-
});
|
|
142
|
-
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
143
|
-
if (!validation.ok) {
|
|
144
|
-
return {
|
|
145
|
-
checked: true,
|
|
146
|
-
ok: false,
|
|
147
|
-
reason: validation.reason,
|
|
148
|
-
httpStatus: validation.status || null,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
const { catalog, apiBase } = await fetchCopilotCatalog(validation.copilotTokenData, {
|
|
152
|
-
fetchImpl,
|
|
153
|
-
signal: controller.signal,
|
|
154
|
-
});
|
|
155
|
-
let upstreamHost = "GitHub Copilot";
|
|
156
|
-
try { upstreamHost = new URL(apiBase).hostname; } catch {}
|
|
157
|
-
return {
|
|
158
|
-
checked: true,
|
|
159
|
-
ok: true,
|
|
160
|
-
login: validation.login || credentials.identity?.login || "",
|
|
161
|
-
models: catalogModels(catalog).length,
|
|
162
|
-
upstreamHost,
|
|
163
|
-
};
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return {
|
|
166
|
-
checked: true,
|
|
167
|
-
ok: false,
|
|
168
|
-
reason: controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error.message,
|
|
169
|
-
};
|
|
170
|
-
} finally {
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function authStatusOnline({
|
|
176
|
-
home = os.homedir(),
|
|
177
|
-
fetchImpl = fetch,
|
|
178
|
-
timeoutMs = 10000,
|
|
179
|
-
} = {}) {
|
|
180
|
-
const local = authStatus({ home });
|
|
181
|
-
const online = await inspectProfileOnline(readStatusCredentials(home), { fetchImpl, timeoutMs });
|
|
182
|
-
return {
|
|
183
|
-
...local,
|
|
184
|
-
profiles: {
|
|
185
|
-
codex: { ...local.profiles.codex, online },
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
}
|
|
10
|
+
export { authStatus, authStatusOnline } from "./auth-status.mjs";
|
|
189
11
|
|
|
190
12
|
function accountLabel(profile) {
|
|
191
13
|
if (!profile.configured) return "not configured";
|
package/src/cli-models.mjs
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import { validateGithubToken } from "./auth.mjs";
|
|
3
|
-
import {
|
|
4
|
-
AUTH_PROFILE_CODEX,
|
|
5
|
-
readAuthProfileCredentials,
|
|
6
|
-
} from "./auth-profile.mjs";
|
|
7
|
-
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
8
1
|
import { status } from "./status.mjs";
|
|
9
2
|
import {
|
|
10
3
|
cliOutputFormat,
|
|
@@ -13,11 +6,7 @@ import {
|
|
|
13
6
|
terminalCell,
|
|
14
7
|
} from "./cli-table.mjs";
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
const SUPPORTED_ENDPOINTS = [
|
|
18
|
-
["responses", new Set(["/responses", "/v1/responses"])],
|
|
19
|
-
["chat", new Set(["/chat/completions"])],
|
|
20
|
-
];
|
|
9
|
+
export { fetchLiveCopilotModels, selectableCopilotModels } from "./live-models.mjs";
|
|
21
10
|
|
|
22
11
|
function safeText(value, fallback = "") {
|
|
23
12
|
const text = String(value ?? "")
|
|
@@ -27,125 +16,6 @@ function safeText(value, fallback = "") {
|
|
|
27
16
|
return text || fallback;
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
function endpointLabels(model) {
|
|
31
|
-
const advertised = new Set(Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : []);
|
|
32
|
-
return SUPPORTED_ENDPOINTS
|
|
33
|
-
.filter(([, values]) => [...values].some((value) => advertised.has(value)))
|
|
34
|
-
.map(([label]) => label);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function selectableCopilotModels(payload) {
|
|
38
|
-
const data = Array.isArray(payload) ? payload : payload?.data;
|
|
39
|
-
if (!Array.isArray(data)) throw new Error("Copilot models response contained no model list");
|
|
40
|
-
|
|
41
|
-
const models = [];
|
|
42
|
-
const seen = new Set();
|
|
43
|
-
for (const model of data) {
|
|
44
|
-
if (model?.model_picker_enabled !== true) continue;
|
|
45
|
-
const policyState = safeText(model?.policy?.state).toLowerCase();
|
|
46
|
-
if (policyState && policyState !== "enabled") continue;
|
|
47
|
-
const capabilityType = safeText(model?.capabilities?.type).toLowerCase();
|
|
48
|
-
if (capabilityType && capabilityType !== "chat") continue;
|
|
49
|
-
|
|
50
|
-
const id = safeText(model?.id);
|
|
51
|
-
const endpoints = endpointLabels(model);
|
|
52
|
-
if (!id.startsWith("gpt-") || id === "codex-auto-review" || !endpoints.length || seen.has(id)) continue;
|
|
53
|
-
seen.add(id);
|
|
54
|
-
models.push({
|
|
55
|
-
id,
|
|
56
|
-
vendor: safeText(model?.vendor || model?.owned_by, "Unknown"),
|
|
57
|
-
endpoints,
|
|
58
|
-
preview: model?.preview === true,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
models.sort((left, right) => left.vendor.localeCompare(right.vendor, "en")
|
|
63
|
-
|| left.id.localeCompare(right.id, "en"));
|
|
64
|
-
return { advertised: data.length, models };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function validationFailure(validation) {
|
|
68
|
-
const httpStatus = validation?.status ? ` (HTTP ${validation.status})` : "";
|
|
69
|
-
return `GitHub Copilot authentication failed: ${validation?.reason || "unknown error"}${httpStatus}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function responseText(response) {
|
|
73
|
-
try {
|
|
74
|
-
return await response.text();
|
|
75
|
-
} catch {
|
|
76
|
-
return "";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function readModelCredentials(home) {
|
|
81
|
-
let credentials;
|
|
82
|
-
try {
|
|
83
|
-
credentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
84
|
-
} catch (error) {
|
|
85
|
-
throw new Error(`Could not read the GitHub token: ${error.message}`);
|
|
86
|
-
}
|
|
87
|
-
if (credentials.valid) return credentials;
|
|
88
|
-
if (!credentials.configured) throw new Error("GitHub token not found. Start ccdx once to log in.");
|
|
89
|
-
if (credentials.reason === "empty_token") throw new Error("GitHub token file is empty. Start ccdx again to log in.");
|
|
90
|
-
throw new Error(`GitHub token is unavailable (${credentials.reason || "unknown"}). Start ccdx again to log in.`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function fetchLiveCopilotModels({
|
|
94
|
-
home = os.homedir(),
|
|
95
|
-
fetchImpl = fetch,
|
|
96
|
-
timeoutMs = DEFAULT_MODELS_TIMEOUT_MS,
|
|
97
|
-
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
98
|
-
} = {}) {
|
|
99
|
-
const credentials = readModelCredentials(home);
|
|
100
|
-
const controller = new AbortController();
|
|
101
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
102
|
-
try {
|
|
103
|
-
const validation = await validateGithubToken(credentials.token, {
|
|
104
|
-
fetchImpl,
|
|
105
|
-
signal: controller.signal,
|
|
106
|
-
});
|
|
107
|
-
if (controller.signal.aborted) throw new Error(`Live model lookup timed out after ${timeoutMs}ms`);
|
|
108
|
-
if (!validation.ok) throw new Error(validationFailure(validation));
|
|
109
|
-
|
|
110
|
-
const tokenData = validation.copilotTokenData;
|
|
111
|
-
const apiBase = parseApiBase(tokenData);
|
|
112
|
-
const headers = buildHeaders({
|
|
113
|
-
token: tokenData.token,
|
|
114
|
-
version: vscodeVersion,
|
|
115
|
-
initiator: "user",
|
|
116
|
-
vision: false,
|
|
117
|
-
});
|
|
118
|
-
const response = await fetchImpl(`${apiBase}/models`, {
|
|
119
|
-
headers,
|
|
120
|
-
redirect: "error",
|
|
121
|
-
signal: controller.signal,
|
|
122
|
-
});
|
|
123
|
-
const body = await responseText(response);
|
|
124
|
-
if (!response.ok) {
|
|
125
|
-
const detail = safeText(body).slice(0, 240);
|
|
126
|
-
throw new Error(`Copilot models endpoint returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let payload;
|
|
130
|
-
try {
|
|
131
|
-
payload = JSON.parse(body);
|
|
132
|
-
} catch {
|
|
133
|
-
throw new Error("Copilot models endpoint returned invalid JSON");
|
|
134
|
-
}
|
|
135
|
-
const catalog = selectableCopilotModels(payload);
|
|
136
|
-
let upstreamHost = "GitHub Copilot";
|
|
137
|
-
try { upstreamHost = new URL(apiBase).hostname; } catch {}
|
|
138
|
-
return { ...catalog, profile: AUTH_PROFILE_CODEX, upstreamHost: safeText(upstreamHost, "GitHub Copilot") };
|
|
139
|
-
} catch (error) {
|
|
140
|
-
if (controller.signal.aborted && !/timed out/.test(error?.message || "")) {
|
|
141
|
-
throw new Error(`Live model lookup timed out after ${timeoutMs}ms`);
|
|
142
|
-
}
|
|
143
|
-
throw error;
|
|
144
|
-
} finally {
|
|
145
|
-
clearTimeout(timer);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
19
|
function liveCatalogHeader(catalog, { commandName, models }) {
|
|
150
20
|
const responses = models.filter((model) => model.endpoints.includes("responses")).length;
|
|
151
21
|
const chat = models.filter((model) => model.endpoints.includes("chat")).length;
|
package/src/cli-table.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { terminalCell } from "./terminal-text.mjs";
|
|
2
|
+
|
|
3
|
+
export { terminalCell } from "./terminal-text.mjs";
|
|
2
4
|
|
|
3
5
|
const DEFAULT_TERMINAL_COLUMNS = 120;
|
|
4
6
|
const COMBINING_MARK = /\p{Mark}/u;
|
|
@@ -50,16 +52,6 @@ function padded(value, width, align) {
|
|
|
50
52
|
return align === "right" ? `${spaces}${value}` : `${value}${spaces}`;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
export function terminalCell(value, { fallback = "—" } = {}) {
|
|
54
|
-
if (value === undefined || value === null) return fallback;
|
|
55
|
-
const text = stripVTControlCharacters(String(value))
|
|
56
|
-
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
57
|
-
.replace(/[\u061c\u200b\u200e\u200f\u202a-\u202e\u2060\u2066-\u2069\ufeff]/g, "")
|
|
58
|
-
.replace(/\s+/g, " ")
|
|
59
|
-
.trim();
|
|
60
|
-
return text || fallback;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
55
|
export function cliOutputFormat(format = "auto", output = process.stdout) {
|
|
64
56
|
if (format === "table" || format === "plain") return format;
|
|
65
57
|
return output?.isTTY === true ? "table" : "plain";
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
sanitizeImageNamespaceCollisionRequest,
|
|
6
6
|
} from "./copilot-responses-policy.mjs";
|
|
7
7
|
import {
|
|
8
|
-
httpError,
|
|
9
8
|
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
10
9
|
readBoundedResponseText,
|
|
11
10
|
} from "./http-transport.mjs";
|
|
11
|
+
import { httpError } from "./http-errors.mjs";
|
|
12
12
|
import {
|
|
13
13
|
finalizeEncryptedHistoryRebase,
|
|
14
14
|
sanitizeEncryptedReasoningRequest,
|
package/src/dashboard-api.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
-
import { fetchLiveCopilotModels } from "./
|
|
3
|
-
import { authStatus } from "./
|
|
2
|
+
import { fetchLiveCopilotModels } from "./live-models.mjs";
|
|
3
|
+
import { authStatus } from "./auth-status.mjs";
|
|
4
4
|
import { redactDiagnosticText } from "./diagnostic-text.mjs";
|
|
5
5
|
import { createRequestAbort, readJsonBody } from "./http-transport.mjs";
|
|
6
|
-
import { isLoopbackAddress, isLoopbackHostHeader } from "./
|
|
6
|
+
import { isLoopbackAddress, isLoopbackHostHeader } from "./security.mjs";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
9
9
|
getTerminalAnimationFrameDelay,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
renderTerminalAnimationFrame,
|
|
12
12
|
TERMINAL_ANIMATION_THEMES,
|
|
13
13
|
} from "./terminal-animation.mjs";
|
|
14
|
-
import { cacheReadTokens, summarizeUsageLogs, usageCacheHitRate } from "./usage.mjs";
|
|
14
|
+
import { cacheReadTokens, summarizeUsageLogs, usageCacheHitRate } from "./usage-store.mjs";
|
|
15
15
|
import { createUsageAnalytics } from "./usage-analytics.mjs";
|
|
16
16
|
import { readUserSettings, terminalAnimationPreference, writeTerminalAnimationTheme } from "./user-settings.mjs";
|
|
17
17
|
|
package/src/dashboard.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { isLoopbackAddress, isLoopbackHostHeader } from "./
|
|
2
|
+
import { isLoopbackAddress, isLoopbackHostHeader } from "./security.mjs";
|
|
3
3
|
|
|
4
4
|
const ASSETS = new Map([
|
|
5
5
|
["/", ["./dashboard/index.html", "text/html; charset=utf-8"]],
|