codex-copilot-dx 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -3
- package/bin/cli.mjs +63 -92
- package/package.json +1 -1
- package/src/adapter.mjs +56 -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 +92 -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 +53 -3
- package/src/profile-model-runtime.mjs +261 -0
- package/src/profile-runtime.mjs +80 -0
- package/src/responses-handler.mjs +7 -6
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isValidModelList,
|
|
3
|
+
loadModelCache,
|
|
4
|
+
saveModelCache,
|
|
5
|
+
} from "./model-cache.mjs";
|
|
6
|
+
import {
|
|
7
|
+
claudeDesktopModelDefsFromCopilotModels,
|
|
8
|
+
codexAutoReviewModelStatus,
|
|
9
|
+
gptModelIdsFromCopilotModels,
|
|
10
|
+
parseModelAliasEnv,
|
|
11
|
+
} from "./models.mjs";
|
|
12
|
+
import { initializeModelRegistry } from "./startup.mjs";
|
|
13
|
+
import { status } from "./status.mjs";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
16
|
+
|
|
17
|
+
function defaultLog(kind, message) {
|
|
18
|
+
console.log(status(kind, message));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parsedCatalog(result) {
|
|
22
|
+
if (result?.status < 200 || result?.status >= 300) {
|
|
23
|
+
throw new Error(`Copilot models returned HTTP ${result?.status}`);
|
|
24
|
+
}
|
|
25
|
+
const models = JSON.parse(result.body);
|
|
26
|
+
if (!isValidModelList(models)) {
|
|
27
|
+
throw new Error("Copilot models response contained no valid models");
|
|
28
|
+
}
|
|
29
|
+
return models;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createProfileModelRuntime({
|
|
33
|
+
codexClient,
|
|
34
|
+
claudeClient = codexClient,
|
|
35
|
+
claudeMode = "inherited",
|
|
36
|
+
claudeCredentialFingerprint = "",
|
|
37
|
+
home,
|
|
38
|
+
env = process.env,
|
|
39
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
40
|
+
log = defaultLog,
|
|
41
|
+
onClaudeModelsChanged = () => {},
|
|
42
|
+
autoReviewModelResolver = () => undefined,
|
|
43
|
+
commandName = "ccdx",
|
|
44
|
+
} = {}) {
|
|
45
|
+
if (!codexClient) throw new Error("codexClient is required");
|
|
46
|
+
if (claudeMode !== "inherited" && claudeMode !== "isolated") {
|
|
47
|
+
throw new Error(`Unsupported Claude model mode: ${claudeMode}`);
|
|
48
|
+
}
|
|
49
|
+
if (claudeMode === "isolated" && claudeClient === codexClient) {
|
|
50
|
+
throw new Error("Isolated Claude models require an isolated Claude client");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const codexRegistry = { modelDefs: undefined, models: undefined, source: "built-in" };
|
|
54
|
+
const claudeRegistry = claudeMode === "inherited"
|
|
55
|
+
? codexRegistry
|
|
56
|
+
: { modelDefs: undefined, models: undefined, source: "built-in" };
|
|
57
|
+
const customClaudeDefs = parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES);
|
|
58
|
+
const refreshTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
59
|
+
? timeoutMs
|
|
60
|
+
: DEFAULT_TIMEOUT_MS;
|
|
61
|
+
|
|
62
|
+
function emit(kind, message) {
|
|
63
|
+
if (typeof log === "function") log(kind, message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function notifyClaudeModels(modelDefs) {
|
|
67
|
+
if (!Array.isArray(modelDefs) || !modelDefs.length) return;
|
|
68
|
+
try {
|
|
69
|
+
const pending = onClaudeModelsChanged(modelDefs);
|
|
70
|
+
pending?.catch?.((error) => emit("warn", `Could not apply refreshed Claude models (${error.message})`));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
emit("warn", `Could not apply refreshed Claude models (${error.message})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applyCatalog({ client, models, registry, source, ownsClaudeModels }) {
|
|
77
|
+
if (!isValidModelList(models)) throw new Error("Copilot models response contained no valid models");
|
|
78
|
+
client.cacheModelEndpoints(models);
|
|
79
|
+
registry.models = models;
|
|
80
|
+
registry.source = source;
|
|
81
|
+
|
|
82
|
+
if (!ownsClaudeModels) return registry.modelDefs;
|
|
83
|
+
const discovered = customClaudeDefs.length
|
|
84
|
+
? customClaudeDefs.map((model) => ({ ...model }))
|
|
85
|
+
: claudeDesktopModelDefsFromCopilotModels(models);
|
|
86
|
+
if (discovered.length) {
|
|
87
|
+
registry.modelDefs = discovered;
|
|
88
|
+
notifyClaudeModels(discovered);
|
|
89
|
+
}
|
|
90
|
+
return registry.modelDefs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function loadCached({ client, profile, registry, ownsClaudeModels }) {
|
|
94
|
+
const credentialFingerprint = profile === "claude"
|
|
95
|
+
? String(claudeCredentialFingerprint || "").trim()
|
|
96
|
+
: "";
|
|
97
|
+
if (profile === "claude" && !credentialFingerprint) return false;
|
|
98
|
+
const cached = loadModelCache({ home, profile, credentialFingerprint });
|
|
99
|
+
if (!cached) return false;
|
|
100
|
+
try {
|
|
101
|
+
applyCatalog({ client, models: cached, registry, source: "cache", ownsClaudeModels });
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function requestCatalog(client) {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timer = setTimeout(() => controller.abort(), refreshTimeoutMs);
|
|
111
|
+
try {
|
|
112
|
+
return parsedCatalog(await client.listModels({ signal: controller.signal }));
|
|
113
|
+
} finally {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function checkCodexCatalog(models) {
|
|
119
|
+
const autoReview = codexAutoReviewModelStatus(models, env, {
|
|
120
|
+
autoReviewModel: autoReviewModelResolver(),
|
|
121
|
+
});
|
|
122
|
+
if (!autoReview.available) {
|
|
123
|
+
emit("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run ${commandName} doctor --compat to verify the live path.`);
|
|
124
|
+
}
|
|
125
|
+
const gptModelIds = gptModelIdsFromCopilotModels(models);
|
|
126
|
+
if (gptModelIds.length) {
|
|
127
|
+
emit("ok", `Refreshed GPT models from GitHub Copilot: ${gptModelIds.join(", ")}`);
|
|
128
|
+
} else {
|
|
129
|
+
emit("warn", "Copilot models response contained no GPT models");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function persist(models, profile) {
|
|
134
|
+
const credentialFingerprint = profile === "claude"
|
|
135
|
+
? String(claudeCredentialFingerprint || "").trim()
|
|
136
|
+
: "";
|
|
137
|
+
if (profile === "claude" && !credentialFingerprint) return;
|
|
138
|
+
try {
|
|
139
|
+
saveModelCache(models, { home, profile, credentialFingerprint });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const label = profile === "codex" ? "Copilot" : "Claude Copilot";
|
|
142
|
+
emit("warn", `Could not persist the ${label} model cache (${error.message})`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function logClaudeCatalog(modelDefs) {
|
|
147
|
+
if (customClaudeDefs.length) {
|
|
148
|
+
emit("info", "Using CCDX_CLAUDE_MODEL_ALIASES for Claude models");
|
|
149
|
+
} else if (modelDefs?.length) {
|
|
150
|
+
emit("ok", `Refreshed Claude models from GitHub Copilot: ${modelDefs.map((model) => model.id).join(", ")}`);
|
|
151
|
+
} else {
|
|
152
|
+
emit("warn", "Copilot models response contained no Claude models; using built-in Claude models");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function fallbackLabel(registry, profile) {
|
|
157
|
+
if (claudeMode === "inherited") {
|
|
158
|
+
return registry.modelDefs?.length ? `${registry.source} model list` : "built-in model list";
|
|
159
|
+
}
|
|
160
|
+
if (isValidModelList(registry.models)) return `${registry.source} model list`;
|
|
161
|
+
return profile === "claude" ? "built-in model list" : "no cached model list";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function refreshProfile({ client, profile, registry, ownsClaudeModels, checksCodex }) {
|
|
165
|
+
const controllerLabel = claudeMode === "inherited"
|
|
166
|
+
? ""
|
|
167
|
+
: `${profile === "codex" ? "Codex" : "Claude"} `;
|
|
168
|
+
try {
|
|
169
|
+
const models = await requestCatalog(client);
|
|
170
|
+
if (checksCodex) checkCodexCatalog(models);
|
|
171
|
+
const modelDefs = applyCatalog({
|
|
172
|
+
client,
|
|
173
|
+
models,
|
|
174
|
+
registry,
|
|
175
|
+
source: "live",
|
|
176
|
+
ownsClaudeModels,
|
|
177
|
+
});
|
|
178
|
+
persist(models, profile);
|
|
179
|
+
if (ownsClaudeModels) logClaudeCatalog(modelDefs);
|
|
180
|
+
return modelDefs;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const fallback = fallbackLabel(registry, profile);
|
|
183
|
+
const timedOut = error?.name === "AbortError" || error?.code === "ABORT_ERR";
|
|
184
|
+
const message = timedOut
|
|
185
|
+
? `${controllerLabel}model refresh timed out after ${refreshTimeoutMs}ms; using ${fallback}`
|
|
186
|
+
: `Could not refresh ${controllerLabel.toLowerCase()}model list; using ${fallback} (${error.message})`;
|
|
187
|
+
emit("warn", message[0].toUpperCase() + message.slice(1));
|
|
188
|
+
return registry.modelDefs;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function refreshCodex() {
|
|
193
|
+
return refreshProfile({
|
|
194
|
+
client: codexClient,
|
|
195
|
+
profile: "codex",
|
|
196
|
+
registry: codexRegistry,
|
|
197
|
+
ownsClaudeModels: claudeMode === "inherited",
|
|
198
|
+
checksCodex: true,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function refreshClaude() {
|
|
203
|
+
if (claudeMode === "inherited") return refreshCodex();
|
|
204
|
+
return refreshProfile({
|
|
205
|
+
client: claudeClient,
|
|
206
|
+
profile: "claude",
|
|
207
|
+
registry: claudeRegistry,
|
|
208
|
+
ownsClaudeModels: true,
|
|
209
|
+
checksCodex: false,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function initializeProfile({ client, profile, registry, ownsClaudeModels, refresh }) {
|
|
214
|
+
return initializeModelRegistry({
|
|
215
|
+
loadCached: () => loadCached({ client, profile, registry, ownsClaudeModels }),
|
|
216
|
+
currentModelDefs: () => registry.modelDefs,
|
|
217
|
+
refresh,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function initialize() {
|
|
222
|
+
const codexInitialization = initializeProfile({
|
|
223
|
+
client: codexClient,
|
|
224
|
+
profile: "codex",
|
|
225
|
+
registry: codexRegistry,
|
|
226
|
+
ownsClaudeModels: claudeMode === "inherited",
|
|
227
|
+
refresh: refreshCodex,
|
|
228
|
+
});
|
|
229
|
+
if (claudeMode === "inherited") {
|
|
230
|
+
const shared = await codexInitialization;
|
|
231
|
+
return { codex: shared, claude: shared };
|
|
232
|
+
}
|
|
233
|
+
const claudeInitialization = initializeProfile({
|
|
234
|
+
client: claudeClient,
|
|
235
|
+
profile: "claude",
|
|
236
|
+
registry: claudeRegistry,
|
|
237
|
+
ownsClaudeModels: true,
|
|
238
|
+
refresh: refreshClaude,
|
|
239
|
+
});
|
|
240
|
+
const [codex, claude] = await Promise.all([codexInitialization, claudeInitialization]);
|
|
241
|
+
return { codex, claude };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function refreshAll() {
|
|
245
|
+
if (claudeMode === "inherited") {
|
|
246
|
+
const shared = await refreshCodex();
|
|
247
|
+
return { codex: shared, claude: shared };
|
|
248
|
+
}
|
|
249
|
+
const [codex, claude] = await Promise.all([refreshCodex(), refreshClaude()]);
|
|
250
|
+
return { codex, claude };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
codexRegistry,
|
|
255
|
+
claudeRegistry,
|
|
256
|
+
initialize,
|
|
257
|
+
refreshAll,
|
|
258
|
+
refreshCodex,
|
|
259
|
+
refreshClaude,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTH_PROFILE_CLAUDE,
|
|
3
|
+
authProfilePaths,
|
|
4
|
+
profileReauthMessage,
|
|
5
|
+
readAuthProfileCredentials,
|
|
6
|
+
} from "./auth-profile.mjs";
|
|
7
|
+
import { createCopilotClient, defaultCopilotClient } from "./copilot.mjs";
|
|
8
|
+
import { githubTokenFingerprint } from "./github-identity.mjs";
|
|
9
|
+
|
|
10
|
+
function safeProfileStatus(credentials) {
|
|
11
|
+
const identity = credentials.identity
|
|
12
|
+
? {
|
|
13
|
+
login: credentials.identity.login,
|
|
14
|
+
id: credentials.identity.id,
|
|
15
|
+
}
|
|
16
|
+
: null;
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
profile: credentials.profile,
|
|
19
|
+
configured: credentials.configured,
|
|
20
|
+
valid: credentials.valid,
|
|
21
|
+
reason: credentials.reason,
|
|
22
|
+
identity: identity ? Object.freeze(identity) : null,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createProfileRuntime({
|
|
27
|
+
home,
|
|
28
|
+
codexClient = defaultCopilotClient,
|
|
29
|
+
} = {}) {
|
|
30
|
+
const unreadableCredentials = () => ({
|
|
31
|
+
profile: AUTH_PROFILE_CLAUDE,
|
|
32
|
+
configured: true,
|
|
33
|
+
valid: false,
|
|
34
|
+
reason: "credential_read_failed",
|
|
35
|
+
token: "",
|
|
36
|
+
identity: null,
|
|
37
|
+
paths: authProfilePaths(AUTH_PROFILE_CLAUDE, { home }),
|
|
38
|
+
});
|
|
39
|
+
const readCredentials = () => {
|
|
40
|
+
try {
|
|
41
|
+
return readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home });
|
|
42
|
+
} catch {
|
|
43
|
+
return unreadableCredentials();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const credentials = readCredentials();
|
|
47
|
+
const claudeProfile = safeProfileStatus(credentials);
|
|
48
|
+
|
|
49
|
+
if (!credentials.configured) {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
codexClient,
|
|
52
|
+
claudeClient: codexClient,
|
|
53
|
+
claudeMode: "inherited",
|
|
54
|
+
claudeProfile,
|
|
55
|
+
claudeCredentialFingerprint: "",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const readRuntimeCredentials = credentials.reason === "credential_read_failed"
|
|
60
|
+
? () => credentials
|
|
61
|
+
: readCredentials;
|
|
62
|
+
const claudeClient = createCopilotClient({
|
|
63
|
+
profile: AUTH_PROFILE_CLAUDE,
|
|
64
|
+
home,
|
|
65
|
+
tokenPath: credentials.paths.tokenPath,
|
|
66
|
+
allowTokenDiscovery: false,
|
|
67
|
+
readGithubCredentials: readRuntimeCredentials,
|
|
68
|
+
reauthMessage: (reason) => `${reason}\n${profileReauthMessage(AUTH_PROFILE_CLAUDE, { home })}`,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
codexClient,
|
|
73
|
+
claudeClient,
|
|
74
|
+
claudeMode: "isolated",
|
|
75
|
+
claudeProfile,
|
|
76
|
+
claudeCredentialFingerprint: credentials.valid
|
|
77
|
+
? githubTokenFingerprint(credentials.token)
|
|
78
|
+
: "",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -34,8 +34,8 @@ const RESPONSES_ONLY_FALLBACK = new Set([
|
|
|
34
34
|
"gpt-5.2-codex",
|
|
35
35
|
]);
|
|
36
36
|
|
|
37
|
-
function isResponsesOnlyModel(model) {
|
|
38
|
-
const endpoints =
|
|
37
|
+
function isResponsesOnlyModel(model, getCachedModelEndpointsFn = getCachedModelEndpoints) {
|
|
38
|
+
const endpoints = getCachedModelEndpointsFn(model);
|
|
39
39
|
if (endpoints) {
|
|
40
40
|
const fakeModel = { supported_endpoints: endpoints };
|
|
41
41
|
if (modelSupportsChatCompletions(fakeModel)) return false;
|
|
@@ -44,8 +44,8 @@ function isResponsesOnlyModel(model) {
|
|
|
44
44
|
return RESPONSES_ONLY_FALLBACK.has(model);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function cachedModelSupportsResponses(model) {
|
|
48
|
-
const endpoints =
|
|
47
|
+
function cachedModelSupportsResponses(model, getCachedModelEndpointsFn = getCachedModelEndpoints) {
|
|
48
|
+
const endpoints = getCachedModelEndpointsFn(model);
|
|
49
49
|
return Array.isArray(endpoints)
|
|
50
50
|
&& (endpoints.includes("/responses") || endpoints.includes("/v1/responses"));
|
|
51
51
|
}
|
|
@@ -78,6 +78,7 @@ export function createResponsesHandler(options) {
|
|
|
78
78
|
acquireRequest,
|
|
79
79
|
autoReviewModelResolver,
|
|
80
80
|
chatCompletionsFn,
|
|
81
|
+
getCachedModelEndpointsFn = getCachedModelEndpoints,
|
|
81
82
|
openAIModelEnv,
|
|
82
83
|
responsesPayloadOptions,
|
|
83
84
|
responsesFn,
|
|
@@ -105,8 +106,8 @@ export function createResponsesHandler(options) {
|
|
|
105
106
|
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
|
|
106
107
|
const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
|
|
107
108
|
const useNativeResponses = requestedModel === CODEX_AUTO_REVIEW_MODEL
|
|
108
|
-
|| isResponsesOnlyModel(upstreamModel)
|
|
109
|
-
|| (usesCustomTools && cachedModelSupportsResponses(upstreamModel));
|
|
109
|
+
|| isResponsesOnlyModel(upstreamModel, getCachedModelEndpointsFn)
|
|
110
|
+
|| (usesCustomTools && cachedModelSupportsResponses(upstreamModel, getCachedModelEndpointsFn));
|
|
110
111
|
if (usesCustomTools && !useNativeResponses) throw unsupportedCustomToolsError(upstreamModel);
|
|
111
112
|
if (useNativeResponses) {
|
|
112
113
|
await proxyCopilotResponses(prepared, req, res, responsesFn, {
|