codex-copilot-dx 0.6.21 → 0.7.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.
Files changed (42) hide show
  1. package/README.md +142 -323
  2. package/bin/cli.mjs +26 -130
  3. package/package.json +2 -8
  4. package/scripts/benchmark.mjs +0 -114
  5. package/src/adapter.mjs +22 -107
  6. package/src/auth-profile.mjs +18 -130
  7. package/src/cli-animation.mjs +108 -0
  8. package/src/cli-auth.mjs +44 -398
  9. package/src/cli-models.mjs +21 -49
  10. package/src/cli-options.mjs +53 -75
  11. package/src/cli-status.mjs +6 -23
  12. package/src/config.mjs +2 -8
  13. package/src/copilot.mjs +2 -2
  14. package/src/doctor.mjs +44 -291
  15. package/src/model-cache.mjs +4 -11
  16. package/src/models.mjs +0 -184
  17. package/src/observability.mjs +1 -38
  18. package/src/profile-model-runtime.mjs +61 -189
  19. package/src/profile-routing.mjs +2 -102
  20. package/src/profile-runtime.mjs +15 -68
  21. package/src/responses-handler.mjs +1 -1
  22. package/src/responses-proxy.mjs +1 -1
  23. package/src/running-adapter.mjs +2 -4
  24. package/src/stream-errors.mjs +3 -6
  25. package/src/stream-performance.mjs +1 -1
  26. package/src/terminal-activity.mjs +40 -34
  27. package/src/terminal-animation.mjs +249 -0
  28. package/src/usage.mjs +0 -31
  29. package/src/user-settings.mjs +50 -0
  30. package/src/anthropic-handler.mjs +0 -195
  31. package/src/anthropic.mjs +0 -387
  32. package/src/claude-config.mjs +0 -57
  33. package/src/claude-desktop-config.mjs +0 -317
  34. package/src/cli-error.mjs +0 -50
  35. package/src/pm-studio-asar.mjs +0 -1244
  36. package/src/pm-studio-bundle-content.mjs +0 -165
  37. package/src/pm-studio-relay.mjs +0 -563
  38. package/src/pm-studio-setup-lock.mjs +0 -336
  39. package/src/pm-studio-setup.mjs +0 -1581
  40. package/src/pm-studio-status.mjs +0 -369
  41. package/src/token-counter-service.mjs +0 -211
  42. package/src/token-counter-worker.mjs +0 -16
package/src/models.mjs CHANGED
@@ -1,100 +1,6 @@
1
- const DEFAULT_CLAUDE_DESKTOP_MODEL_DEFS = [
2
- {
3
- id: "claude-sonnet-5",
4
- upstream: "claude-sonnet-5",
5
- displayName: "Claude Sonnet 5",
6
- createdAt: "2026-06-30T00:00:00Z",
7
- maxInputTokens: 1000000,
8
- maxOutputTokens: 64000,
9
- },
10
- {
11
- id: "claude-sonnet-4.6",
12
- upstream: "claude-sonnet-4.6",
13
- displayName: "Claude Sonnet 4.6",
14
- createdAt: "2026-02-17T00:00:00Z",
15
- maxInputTokens: 200000,
16
- maxOutputTokens: 8192,
17
- },
18
- {
19
- id: "claude-opus-4.8",
20
- upstream: "claude-opus-4.8",
21
- displayName: "Claude Opus 4.8",
22
- createdAt: "2026-05-28T00:00:00Z",
23
- maxInputTokens: 200000,
24
- maxOutputTokens: 8192,
25
- },
26
- {
27
- id: "claude-haiku-4.5",
28
- upstream: "claude-haiku-4.5",
29
- displayName: "Claude Haiku 4.5",
30
- createdAt: "2025-10-01T00:00:00Z",
31
- maxInputTokens: 200000,
32
- maxOutputTokens: 8192,
33
- },
34
- ];
35
-
36
- const DEFAULT_CLAUDE_MODEL_ALIAS_DEFS = [
37
- {
38
- id: "claude-sonnet-4-6",
39
- upstream: "claude-sonnet-4.6",
40
- displayName: "Claude Sonnet 4.6",
41
- createdAt: "2026-02-17T00:00:00Z",
42
- maxInputTokens: 200000,
43
- maxOutputTokens: 8192,
44
- },
45
- {
46
- id: "claude-opus-4-8",
47
- upstream: "claude-opus-4.8",
48
- displayName: "Claude Opus 4.8",
49
- createdAt: "2026-05-28T00:00:00Z",
50
- maxInputTokens: 200000,
51
- maxOutputTokens: 8192,
52
- },
53
- {
54
- id: "claude-haiku-4-5",
55
- upstream: "claude-haiku-4.5",
56
- displayName: "Claude Haiku 4.5",
57
- createdAt: "2025-10-01T00:00:00Z",
58
- maxInputTokens: 200000,
59
- maxOutputTokens: 8192,
60
- },
61
- ];
62
-
63
1
  export const CODEX_AUTO_REVIEW_MODEL = "codex-auto-review";
64
2
  export const DEFAULT_CODEX_AUTO_REVIEW_MODEL = "gpt-5.5";
65
3
 
66
- function cleanList(value) {
67
- return String(value || "")
68
- .split(",")
69
- .map((item) => item.trim())
70
- .filter(Boolean);
71
- }
72
-
73
- export function parseModelAliasEnv(value) {
74
- const aliases = [];
75
- for (const entry of cleanList(value)) {
76
- const [id, upstream] = entry.split("=", 2).map((part) => part?.trim());
77
- if (!id || !upstream) continue;
78
- aliases.push({
79
- id,
80
- upstream,
81
- displayName: id,
82
- createdAt: "2025-01-01T00:00:00Z",
83
- maxInputTokens: 200000,
84
- maxOutputTokens: 8192,
85
- });
86
- }
87
- return aliases;
88
- }
89
-
90
- function cloneModelDefs(modelDefs) {
91
- return modelDefs.map((model) => ({ ...model }));
92
- }
93
-
94
- function numericLimit(value, fallback) {
95
- return Number.isFinite(value) && value > 0 ? value : fallback;
96
- }
97
-
98
4
  function hasOpenAIEndpoint(model) {
99
5
  const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
100
6
  return endpoints.includes("/responses") || endpoints.includes("/v1/responses") || endpoints.includes("/chat/completions");
@@ -152,24 +58,6 @@ function uniqueIds(models, predicate) {
152
58
  return ids;
153
59
  }
154
60
 
155
- export function isClaudeCopilotCatalogEntry(model) {
156
- const id = String(model?.id || "").trim();
157
- const vendor = String(model?.vendor || model?.owned_by || "").trim().toLowerCase();
158
- return Boolean(id) && (id.toLowerCase().startsWith("claude-") || vendor === "anthropic");
159
- }
160
-
161
- export function isClaudeCopilotModel(model) {
162
- const policy = String(model?.policy?.state || "").trim().toLowerCase();
163
- const declaredOwners = [model?.vendor, model?.owned_by]
164
- .map((value) => String(value || "").trim().toLowerCase())
165
- .filter(Boolean);
166
- return isClaudeCopilotCatalogEntry(model)
167
- && declaredOwners.every((owner) => owner === "anthropic")
168
- && model?.model_picker_enabled !== false
169
- && (!policy || policy === "enabled")
170
- && modelSupportsChatCompletions(model);
171
- }
172
-
173
61
  export function gptModelIdsFromCopilotModels(models) {
174
62
  return uniqueIds(copilotModelData(models), (model) => {
175
63
  const id = String(model?.id || "").trim();
@@ -190,60 +78,6 @@ export function responsesModelIdsFromCopilotModels(models) {
190
78
  });
191
79
  }
192
80
 
193
- export function claudeDesktopModelDefsFromCopilotModels(models) {
194
- const data = copilotModelData(models);
195
-
196
- const defs = [];
197
- const seen = new Set();
198
- for (const model of data) {
199
- if (!isClaudeCopilotModel(model)) continue;
200
- const id = String(model.id || "").trim();
201
- if (seen.has(id)) continue;
202
- seen.add(id);
203
-
204
- const limits = model.capabilities?.limits || {};
205
- defs.push({
206
- id,
207
- upstream: id,
208
- displayName: String(model.name || id),
209
- createdAt: "2025-01-01T00:00:00Z",
210
- maxInputTokens: numericLimit(limits.max_context_window_tokens, numericLimit(limits.max_prompt_tokens, 200000)),
211
- maxOutputTokens: numericLimit(limits.max_output_tokens, 8192),
212
- });
213
- }
214
-
215
- return defs;
216
- }
217
-
218
- export function claudeDesktopModelDefs(env = process.env, options = {}) {
219
- const custom = parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES);
220
- if (custom.length) return custom;
221
- if (Array.isArray(options.modelDefs) && options.modelDefs.length) return cloneModelDefs(options.modelDefs);
222
- return cloneModelDefs(DEFAULT_CLAUDE_DESKTOP_MODEL_DEFS);
223
- }
224
-
225
- export function claudeDesktopModelIds(env = process.env, options = {}) {
226
- return claudeDesktopModelDefs(env, options).map((model) => model.id);
227
- }
228
-
229
- function claudeModelResolutionDefs(env = process.env, options = {}) {
230
- const defs = claudeDesktopModelDefs(env, options);
231
- if (parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES).length) return defs;
232
-
233
- const upstreamIds = new Set(defs.map((model) => model.upstream));
234
- const aliases = DEFAULT_CLAUDE_MODEL_ALIAS_DEFS.filter((alias) => upstreamIds.has(alias.upstream));
235
- return [...defs, ...aliases];
236
- }
237
-
238
- export function resolveAnthropicModel(model, env = process.env, options = {}) {
239
- const requestedModel = String(model || "");
240
- const match = claudeModelResolutionDefs(env, options).find((entry) => entry.id === requestedModel);
241
- return {
242
- requestedModel,
243
- upstreamModel: match?.upstream || requestedModel,
244
- };
245
- }
246
-
247
81
  export function resolveOpenAIModel(model, env = process.env, options = {}) {
248
82
  const requestedModel = String(model || "");
249
83
  if (requestedModel !== CODEX_AUTO_REVIEW_MODEL) {
@@ -269,21 +103,3 @@ export function codexAutoReviewModelStatus(models, env = process.env, options =
269
103
  reason: available ? "" : "model does not advertise a Responses endpoint",
270
104
  };
271
105
  }
272
-
273
- export function anthropicModelInfo(model) {
274
- return {
275
- id: model.id,
276
- type: "model",
277
- display_name: model.displayName || model.id,
278
- created_at: model.createdAt || "2025-01-01T00:00:00Z",
279
- max_input_tokens: model.maxInputTokens || 200000,
280
- max_tokens: model.maxOutputTokens || 8192,
281
- };
282
- }
283
-
284
- export function claudeDesktopModelsResponse(env = process.env, options = {}) {
285
- return {
286
- object: "list",
287
- data: claudeDesktopModelDefs(env, options).map(anthropicModelInfo),
288
- };
289
- }
@@ -13,10 +13,6 @@ const ROUTE_NAMES = Object.freeze([
13
13
  "responses",
14
14
  "responses_compact",
15
15
  "models",
16
- "messages",
17
- "messages_count_tokens",
18
- "pm_models",
19
- "pm_chat_completions",
20
16
  "not_found",
21
17
  ]);
22
18
 
@@ -98,10 +94,6 @@ export function classifyAdapterRoute(method, pathname) {
98
94
  if (method === "POST" && pathname === "/v1/responses") return "responses";
99
95
  if (method === "POST" && pathname === "/v1/responses/compact") return "responses_compact";
100
96
  if (method === "GET" && pathname === "/v1/models") return "models";
101
- if (method === "POST" && pathname === "/v1/messages") return "messages";
102
- if (method === "POST" && pathname === "/v1/messages/count_tokens") return "messages_count_tokens";
103
- if (method === "GET" && pathname === "/pm-ccdx/models") return "pm_models";
104
- if (method === "POST" && pathname === "/pm-ccdx/chat/completions") return "pm_chat_completions";
105
97
  return "not_found";
106
98
  }
107
99
 
@@ -119,7 +111,6 @@ function modelRegistryStatus(modelRegistry) {
119
111
  const result = {
120
112
  source: String(modelRegistry?.source || "built-in"),
121
113
  models: Array.isArray(modelData) ? modelData.length : 0,
122
- claude_models: Array.isArray(modelRegistry?.modelDefs) ? modelRegistry.modelDefs.length : 0,
123
114
  };
124
115
  if (typeof modelRegistry?.cacheState === "string") {
125
116
  const savedAtMs = Number(modelRegistry.cacheSavedAtMs);
@@ -158,15 +149,6 @@ function safeClientRuntimeStatus(client) {
158
149
  };
159
150
  }
160
151
 
161
- function safeProfileCurrent(checkProfileCurrent) {
162
- if (typeof checkProfileCurrent !== "function") return false;
163
- try {
164
- return checkProfileCurrent() === true;
165
- } catch {
166
- return false;
167
- }
168
- }
169
-
170
152
  export function runtimeStatusPayload({
171
153
  metrics,
172
154
  streamPerformance,
@@ -174,24 +156,11 @@ export function runtimeStatusPayload({
174
156
  imagePressure,
175
157
  modelRegistry,
176
158
  codexClient,
177
- claudeClient,
178
159
  codexModelRegistry,
179
- claudeModelRegistry,
180
- claudeMode = "inherited",
181
- isClaudeProfileCurrent,
182
160
  } = {}) {
183
161
  const memory = process.memoryUsage();
184
- const resolvedClaudeMode = claudeMode === "isolated" ? "isolated" : "inherited";
185
162
  const codexRuntime = safeClientRuntimeStatus(codexClient);
186
163
  const codexModels = modelRegistryStatus(codexModelRegistry || modelRegistry);
187
- const claudeRuntime = resolvedClaudeMode === "isolated"
188
- ? safeClientRuntimeStatus(claudeClient)
189
- : codexRuntime;
190
- const claudeModels = resolvedClaudeMode === "isolated"
191
- ? modelRegistryStatus(claudeModelRegistry)
192
- : codexModels;
193
- const claudeProfileCurrent = resolvedClaudeMode === "isolated"
194
- && safeProfileCurrent(isClaudeProfileCurrent);
195
164
  return {
196
165
  ...adapterHealthPayload(),
197
166
  uptime_ms: Math.round(process.uptime() * 1000),
@@ -212,14 +181,8 @@ export function runtimeStatusPayload({
212
181
  models: codexModels,
213
182
  profiles: {
214
183
  codex: { mode: "legacy", client: codexRuntime, models: codexModels },
215
- claude: {
216
- mode: resolvedClaudeMode,
217
- profile_current: claudeProfileCurrent,
218
- client: claudeRuntime,
219
- models: claudeModels,
220
- },
221
184
  },
222
- routing: profileRouting({ claudeMode: resolvedClaudeMode }),
185
+ routing: profileRouting(),
223
186
  limits: {
224
187
  max_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxBodyBytes,
225
188
  max_decoded_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxDecodedBodyBytes,
@@ -4,10 +4,8 @@ import {
4
4
  saveModelCache,
5
5
  } from "./model-cache.mjs";
6
6
  import {
7
- claudeDesktopModelDefsFromCopilotModels,
8
7
  codexAutoReviewModelStatus,
9
8
  gptModelIdsFromCopilotModels,
10
- parseModelAliasEnv,
11
9
  } from "./models.mjs";
12
10
  import { initializeModelRegistry } from "./startup.mjs";
13
11
  import { status } from "./status.mjs";
@@ -36,7 +34,6 @@ function parsedCatalog(result) {
36
34
 
37
35
  function createModelRegistry() {
38
36
  return {
39
- modelDefs: undefined,
40
37
  models: undefined,
41
38
  source: "built-in",
42
39
  cacheState: "none",
@@ -48,12 +45,6 @@ function createModelRegistry() {
48
45
  };
49
46
  }
50
47
 
51
- function sameModelDefs(left, right) {
52
- if (left === right) return true;
53
- if (!Array.isArray(left) || !Array.isArray(right)) return false;
54
- return JSON.stringify(left) === JSON.stringify(right);
55
- }
56
-
57
48
  function safeRefreshError(error, { timedOut = false, timeoutMs } = {}) {
58
49
  if (timedOut) return `Model refresh timed out after ${timeoutMs}ms`;
59
50
  const raw = String(error?.message || error?.name || "Model refresh failed")
@@ -66,97 +57,63 @@ function safeRefreshError(error, { timedOut = false, timeoutMs } = {}) {
66
57
 
67
58
  export function createProfileModelRuntime({
68
59
  codexClient,
69
- claudeClient = codexClient,
70
- claudeMode = "inherited",
71
60
  codexCredentialFingerprint = "",
72
- claudeCredentialFingerprint = "",
73
61
  home,
74
62
  env = process.env,
75
63
  timeoutMs = DEFAULT_TIMEOUT_MS,
76
64
  log = defaultLog,
77
- onClaudeModelsChanged = () => {},
78
65
  autoReviewModelResolver = () => undefined,
79
66
  commandName = "ccdx",
80
67
  } = {}) {
81
68
  if (!codexClient) throw new Error("codexClient is required");
82
- if (claudeMode !== "inherited" && claudeMode !== "isolated") {
83
- throw new Error(`Unsupported Claude model mode: ${claudeMode}`);
84
- }
85
- if (claudeMode === "isolated" && claudeClient === codexClient) {
86
- throw new Error("Isolated Claude models require an isolated Claude client");
87
- }
88
69
 
89
70
  const codexRegistry = createModelRegistry();
90
- const claudeRegistry = claudeMode === "inherited"
91
- ? codexRegistry
92
- : createModelRegistry();
93
- const customClaudeDefs = parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES);
94
- const refreshFlights = new Map();
95
- const refreshGenerations = new Map();
96
71
  const refreshTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
97
72
  ? timeoutMs
98
73
  : DEFAULT_TIMEOUT_MS;
74
+ let refreshFlight = null;
75
+ let refreshGeneration = 0;
99
76
 
100
77
  function emit(kind, message) {
101
78
  if (typeof log === "function") log(kind, message);
102
79
  }
103
80
 
104
- function notifyClaudeModels(modelDefs) {
105
- if (!Array.isArray(modelDefs) || !modelDefs.length) return;
106
- try {
107
- const pending = onClaudeModelsChanged(modelDefs);
108
- pending?.catch?.((error) => emit("warn", `Could not apply refreshed Claude models (${error.message})`));
109
- } catch (error) {
110
- emit("warn", `Could not apply refreshed Claude models (${error.message})`);
111
- }
112
- }
113
-
114
- function applyCatalog({ client, models, registry, source, ownsClaudeModels }) {
81
+ function applyCatalog(models, source) {
115
82
  if (!isValidModelList(models)) throw new Error("Copilot models response contained no valid models");
116
- client.cacheModelEndpoints(models);
117
- registry.models = models;
118
- registry.source = source;
119
-
120
- if (!ownsClaudeModels) return registry.modelDefs;
121
- const discovered = customClaudeDefs.length
122
- ? customClaudeDefs.map((model) => ({ ...model }))
123
- : claudeDesktopModelDefsFromCopilotModels(models);
124
- if (discovered.length) {
125
- const changed = !sameModelDefs(registry.modelDefs, discovered);
126
- registry.modelDefs = discovered;
127
- if (changed) notifyClaudeModels(discovered);
128
- }
129
- return registry.modelDefs;
83
+ codexClient.cacheModelEndpoints(models);
84
+ codexRegistry.models = models;
85
+ codexRegistry.source = source;
86
+ return models;
130
87
  }
131
88
 
132
- function loadCached({ client, profile, registry, ownsClaudeModels }) {
133
- const credentialFingerprint = profile === "claude"
134
- ? String(claudeCredentialFingerprint || "").trim()
135
- : String(codexCredentialFingerprint || "").trim();
136
- if (profile === "claude" && !credentialFingerprint) return false;
137
- const cached = loadModelCacheEntry({ home, profile, credentialFingerprint });
89
+ function loadCached() {
90
+ const cached = loadModelCacheEntry({
91
+ home,
92
+ profile: "codex",
93
+ credentialFingerprint: String(codexCredentialFingerprint || "").trim(),
94
+ });
138
95
  if (!cached) return false;
139
96
  try {
140
- applyCatalog({ client, models: cached.models, registry, source: "cache", ownsClaudeModels });
141
- registry.cacheState = cached.state;
142
- registry.cacheSavedAtMs = cached.savedAtMs;
97
+ applyCatalog(cached.models, "cache");
98
+ codexRegistry.cacheState = cached.state;
99
+ codexRegistry.cacheSavedAtMs = cached.savedAtMs;
143
100
  return { loaded: true, stale: cached.state === "stale" };
144
101
  } catch {
145
102
  return false;
146
103
  }
147
104
  }
148
105
 
149
- async function requestCatalog(client) {
106
+ async function requestCatalog() {
150
107
  const controller = new AbortController();
151
108
  const timer = setTimeout(() => controller.abort(), refreshTimeoutMs);
152
109
  try {
153
- return parsedCatalog(await client.listModels({ signal: controller.signal }));
110
+ return parsedCatalog(await codexClient.listModels({ signal: controller.signal }));
154
111
  } finally {
155
112
  clearTimeout(timer);
156
113
  }
157
114
  }
158
115
 
159
- function checkCodexCatalog(models) {
116
+ function checkCatalog(models) {
160
117
  const autoReview = codexAutoReviewModelStatus(models, env, {
161
118
  autoReviewModel: autoReviewModelResolver(),
162
119
  });
@@ -171,166 +128,81 @@ export function createProfileModelRuntime({
171
128
  }
172
129
  }
173
130
 
174
- function persist(models, profile, registry, generation) {
175
- const credentialFingerprint = profile === "claude"
176
- ? String(claudeCredentialFingerprint || "").trim()
177
- : String(codexCredentialFingerprint || "").trim();
178
- if (profile === "claude" && !credentialFingerprint) return;
131
+ function persist(models, generation) {
179
132
  try {
180
133
  const saved = saveModelCache(models, {
181
134
  home,
182
- profile,
183
- credentialFingerprint,
184
- isCurrent: () => refreshGenerations.get(profile) === generation,
135
+ profile: "codex",
136
+ credentialFingerprint: String(codexCredentialFingerprint || "").trim(),
137
+ isCurrent: () => refreshGeneration === generation,
185
138
  });
186
139
  if (saved) {
187
- registry.cacheState = "fresh";
188
- registry.cacheSavedAtMs = Date.now();
140
+ codexRegistry.cacheState = "fresh";
141
+ codexRegistry.cacheSavedAtMs = Date.now();
189
142
  }
190
143
  } catch (error) {
191
- const label = profile === "codex" ? "Copilot" : "Claude Copilot";
192
- emit("warn", `Could not persist the ${label} model cache (${error.message})`);
193
- }
194
- }
195
-
196
- function logClaudeCatalog(modelDefs) {
197
- if (customClaudeDefs.length) {
198
- emit("info", "Using CCDX_CLAUDE_MODEL_ALIASES for Claude models");
199
- } else if (modelDefs?.length) {
200
- emit("ok", `Refreshed Claude models from GitHub Copilot: ${modelDefs.map((model) => model.id).join(", ")}`);
201
- } else {
202
- emit("warn", "Copilot models response contained no Claude models; using built-in Claude models");
203
- }
204
- }
205
-
206
- function fallbackLabel(registry, profile) {
207
- if (claudeMode === "inherited") {
208
- return registry.modelDefs?.length ? `${registry.source} model list` : "built-in model list";
144
+ emit("warn", `Could not persist the Copilot model cache (${error.message})`);
209
145
  }
210
- if (isValidModelList(registry.models)) return `${registry.source} model list`;
211
- return profile === "claude" ? "built-in model list" : "no cached model list";
212
146
  }
213
147
 
214
- async function performRefresh({ client, profile, registry, ownsClaudeModels, checksCodex }, generation) {
215
- const controllerLabel = claudeMode === "inherited"
216
- ? ""
217
- : `${profile === "codex" ? "Codex" : "Claude"} `;
148
+ async function performRefresh(generation) {
218
149
  try {
219
- const models = await requestCatalog(client);
220
- if (refreshGenerations.get(profile) !== generation) return registry.modelDefs;
221
- if (checksCodex) checkCodexCatalog(models);
222
- const modelDefs = applyCatalog({
223
- client,
224
- models,
225
- registry,
226
- source: "live",
227
- ownsClaudeModels,
228
- });
229
- persist(models, profile, registry, generation);
230
- registry.lastError = null;
231
- registry.lastErrorAtMs = null;
232
- if (ownsClaudeModels) logClaudeCatalog(modelDefs);
233
- return modelDefs;
150
+ const models = await requestCatalog();
151
+ if (refreshGeneration !== generation) return codexRegistry.models;
152
+ checkCatalog(models);
153
+ applyCatalog(models, "live");
154
+ persist(models, generation);
155
+ codexRegistry.lastError = null;
156
+ codexRegistry.lastErrorAtMs = null;
157
+ return models;
234
158
  } catch (error) {
235
- if (refreshGenerations.get(profile) !== generation) return registry.modelDefs;
236
- const fallback = fallbackLabel(registry, profile);
159
+ if (refreshGeneration !== generation) return codexRegistry.models;
237
160
  const timedOut = error?.name === "AbortError" || error?.code === "ABORT_ERR";
238
161
  const diagnostic = safeRefreshError(error, { timedOut, timeoutMs: refreshTimeoutMs });
239
- registry.lastError = diagnostic;
240
- registry.lastErrorAtMs = Date.now();
241
- const message = timedOut
242
- ? `${controllerLabel}model refresh timed out after ${refreshTimeoutMs}ms; using ${fallback}`
243
- : `Could not refresh ${controllerLabel.toLowerCase()}model list; using ${fallback} (${diagnostic})`;
244
- emit("warn", message[0].toUpperCase() + message.slice(1));
245
- return registry.modelDefs;
162
+ codexRegistry.lastError = diagnostic;
163
+ codexRegistry.lastErrorAtMs = Date.now();
164
+ const fallback = isValidModelList(codexRegistry.models)
165
+ ? `${codexRegistry.source} model list`
166
+ : "no cached model list";
167
+ emit("warn", timedOut
168
+ ? `Model refresh timed out after ${refreshTimeoutMs}ms; using ${fallback}`
169
+ : `Could not refresh model list; using ${fallback} (${diagnostic})`);
170
+ return codexRegistry.models;
246
171
  }
247
172
  }
248
173
 
249
- function refreshProfile(options) {
250
- const { profile, registry } = options;
251
- const existing = refreshFlights.get(profile);
252
- if (existing) return existing.promise;
253
-
254
- const generation = (refreshGenerations.get(profile) || 0) + 1;
255
- refreshGenerations.set(profile, generation);
256
- registry.generation = generation;
257
- registry.refreshInFlight = true;
258
- const flight = { generation, promise: null };
259
- refreshFlights.set(profile, flight);
260
- flight.promise = performRefresh(options, generation).finally(() => {
261
- if (refreshFlights.get(profile) === flight) refreshFlights.delete(profile);
262
- if (refreshGenerations.get(profile) === generation) registry.refreshInFlight = false;
263
- });
264
- return flight.promise;
265
- }
266
-
267
174
  function refreshCodex() {
268
- return refreshProfile({
269
- client: codexClient,
270
- profile: "codex",
271
- registry: codexRegistry,
272
- ownsClaudeModels: claudeMode === "inherited",
273
- checksCodex: true,
274
- });
275
- }
276
-
277
- function refreshClaude() {
278
- if (claudeMode === "inherited") return refreshCodex();
279
- return refreshProfile({
280
- client: claudeClient,
281
- profile: "claude",
282
- registry: claudeRegistry,
283
- ownsClaudeModels: true,
284
- checksCodex: false,
285
- });
286
- }
287
-
288
- function initializeProfile({ client, profile, registry, ownsClaudeModels, refresh }) {
289
- return initializeModelRegistry({
290
- loadCached: () => loadCached({ client, profile, registry, ownsClaudeModels }),
291
- currentModelDefs: () => registry.modelDefs,
292
- refresh,
175
+ if (refreshFlight) return refreshFlight;
176
+ const generation = refreshGeneration + 1;
177
+ refreshGeneration = generation;
178
+ codexRegistry.generation = generation;
179
+ codexRegistry.refreshInFlight = true;
180
+ const flight = performRefresh(generation).finally(() => {
181
+ if (refreshFlight === flight) refreshFlight = null;
182
+ if (refreshGeneration === generation) codexRegistry.refreshInFlight = false;
293
183
  });
184
+ refreshFlight = flight;
185
+ return flight;
294
186
  }
295
187
 
296
188
  async function initialize() {
297
- const codexInitialization = initializeProfile({
298
- client: codexClient,
299
- profile: "codex",
300
- registry: codexRegistry,
301
- ownsClaudeModels: claudeMode === "inherited",
189
+ const initialized = await initializeModelRegistry({
190
+ loadCached,
191
+ currentModelDefs: () => codexRegistry.models,
302
192
  refresh: refreshCodex,
303
193
  });
304
- if (claudeMode === "inherited") {
305
- const shared = await codexInitialization;
306
- return { codex: shared, claude: shared };
307
- }
308
- const claudeInitialization = initializeProfile({
309
- client: claudeClient,
310
- profile: "claude",
311
- registry: claudeRegistry,
312
- ownsClaudeModels: true,
313
- refresh: refreshClaude,
314
- });
315
- const [codex, claude] = await Promise.all([codexInitialization, claudeInitialization]);
316
- return { codex, claude };
194
+ const { modelDefs: models, ...state } = initialized;
195
+ return { codex: { ...state, models } };
317
196
  }
318
197
 
319
198
  async function refreshAll() {
320
- if (claudeMode === "inherited") {
321
- const shared = await refreshCodex();
322
- return { codex: shared, claude: shared };
323
- }
324
- const [codex, claude] = await Promise.all([refreshCodex(), refreshClaude()]);
325
- return { codex, claude };
199
+ return { codex: await refreshCodex() };
326
200
  }
327
201
 
328
202
  return {
329
203
  codexRegistry,
330
- claudeRegistry,
331
204
  initialize,
332
205
  refreshAll,
333
206
  refreshCodex,
334
- refreshClaude,
335
207
  };
336
208
  }