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.
@@ -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 = getCachedModelEndpoints(model);
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 = getCachedModelEndpoints(model);
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, {
@@ -5,6 +5,7 @@ export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
5
5
  export const ADAPTER_STATUS_PATH = "/_ccdx/status";
6
6
  export const ADAPTER_PROTOCOL_VERSION = 2;
7
7
  export const ADAPTER_VERSION = localPackageVersion();
8
+ export const ADAPTER_CAPABILITIES = Object.freeze(["pm_studio_relay_v1"]);
8
9
  const ADAPTER_INSTANCE_ID = randomUUID();
9
10
 
10
11
  export function adapterHealthPayload() {
@@ -14,6 +15,7 @@ export function adapterHealthPayload() {
14
15
  pid: process.pid,
15
16
  version: ADAPTER_VERSION,
16
17
  protocol_version: ADAPTER_PROTOCOL_VERSION,
18
+ capabilities: [...ADAPTER_CAPABILITIES],
17
19
  instance_id: ADAPTER_INSTANCE_ID,
18
20
  };
19
21
  }
@@ -37,6 +39,7 @@ export async function checkRunningAdapter({
37
39
  timeoutMs = 500,
38
40
  expectedVersion = ADAPTER_VERSION,
39
41
  expectedProtocolVersion = ADAPTER_PROTOCOL_VERSION,
42
+ requiredCapabilities = ADAPTER_CAPABILITIES,
40
43
  } = {}) {
41
44
  const baseUrl = adapterBaseUrl(host, port);
42
45
  const ctrl = new AbortController();
@@ -62,6 +65,19 @@ export async function checkRunningAdapter({
62
65
  expectedProtocolVersion,
63
66
  };
64
67
  }
68
+ const capabilities = new Set(Array.isArray(data.capabilities) ? data.capabilities : []);
69
+ if (requiredCapabilities.some((capability) => !capabilities.has(capability))) {
70
+ return {
71
+ ok: false,
72
+ incompatible: true,
73
+ baseUrl,
74
+ status: resp.status,
75
+ data,
76
+ expectedVersion,
77
+ expectedProtocolVersion,
78
+ requiredCapabilities: [...requiredCapabilities],
79
+ };
80
+ }
65
81
  return { ok: true, baseUrl, data };
66
82
  } catch (e) {
67
83
  return { ok: false, baseUrl, error: e };