codex-copilot-dx 0.4.11 → 0.4.13

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/bin/cli.mjs CHANGED
@@ -5,7 +5,8 @@ import { ensureCodexConfig } from "../src/config.mjs";
5
5
  import { ensureClaudeConfig } from "../src/claude-config.mjs";
6
6
  import { applyClaudeDesktopConfig, formatClaudeDesktopApplyResult, generatedClaudeDesktopApiKey } from "../src/claude-desktop-config.mjs";
7
7
  import { startAdapter } from "../src/adapter.mjs";
8
- import { refreshVSCodeVersion } from "../src/copilot.mjs";
8
+ import { listModels, refreshVSCodeVersion } from "../src/copilot.mjs";
9
+ import { claudeDesktopModelDefsFromCopilotModels, claudeDesktopModelIds, parseModelAliasEnv } from "../src/models.mjs";
9
10
  import { status } from "../src/status.mjs";
10
11
  import { printUsageSummary } from "../src/usage.mjs";
11
12
  import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
@@ -13,6 +14,7 @@ import { runDoctor } from "../src/doctor.mjs";
13
14
 
14
15
  const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
15
16
  const ADAPTER_HOST = process.env.ADAPTER_HOST || "127.0.0.1";
17
+ const MODEL_REFRESH_TIMEOUT_MS = parseInt(process.env.CCDX_MODEL_REFRESH_TIMEOUT_MS || "5000", 10);
16
18
  const LOCAL_VERSION = localPackageVersion();
17
19
  const command = process.argv[2];
18
20
  const CONFIGURE_CLAUDE_DESKTOP = command === "--configure-claude-desktop" || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
@@ -21,6 +23,31 @@ function isLoopbackHost(host) {
21
23
  return ["127.0.0.1", "localhost", "::1"].includes(String(host).toLowerCase());
22
24
  }
23
25
 
26
+ async function refreshClaudeDesktopModelDefs() {
27
+ if (parseModelAliasEnv(process.env.CCDX_CLAUDE_MODEL_ALIASES).length) {
28
+ console.log(status("info", "Using CCDX_CLAUDE_MODEL_ALIASES for Claude models"));
29
+ return undefined;
30
+ }
31
+
32
+ const ctrl = new AbortController();
33
+ const timer = setTimeout(() => ctrl.abort(), MODEL_REFRESH_TIMEOUT_MS);
34
+ try {
35
+ const { status: httpStatus, body } = await listModels({ signal: ctrl.signal });
36
+ if (httpStatus < 200 || httpStatus >= 300) {
37
+ throw new Error(`Copilot models returned HTTP ${httpStatus}`);
38
+ }
39
+ const modelDefs = claudeDesktopModelDefsFromCopilotModels(JSON.parse(body));
40
+ if (!modelDefs.length) throw new Error("Copilot models response contained no Claude models");
41
+ console.log(status("ok", `Refreshed Claude models from GitHub Copilot: ${modelDefs.map((model) => model.id).join(", ")}`));
42
+ return modelDefs;
43
+ } catch (e) {
44
+ console.log(status("warn", `Could not refresh Claude models; using built-in model list (${e.message})`));
45
+ return undefined;
46
+ } finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+
24
51
  if (command === "--version" || command === "-v" || command === "version") {
25
52
  console.log(`codex-copilot-dx v${LOCAL_VERSION}`);
26
53
  process.exit(0);
@@ -53,6 +80,7 @@ try {
53
80
 
54
81
  // Refresh the VS Code version in the background; fallback is non-blocking.
55
82
  refreshVSCodeVersion();
83
+ const claudeDesktopModelDefs = await refreshClaudeDesktopModelDefs();
56
84
 
57
85
  if (!isLoopbackHost(ADAPTER_HOST)) {
58
86
  console.log(status("warn", `ADAPTER_HOST=${ADAPTER_HOST} exposes the adapter beyond loopback. Use only on trusted networks.`));
@@ -63,7 +91,7 @@ try {
63
91
  : (process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || "");
64
92
 
65
93
  // Start the in-process adapter.
66
- await startAdapter(ADAPTER_PORT, ADAPTER_HOST, { claudeDesktopApiKey });
94
+ await startAdapter(ADAPTER_PORT, ADAPTER_HOST, { claudeDesktopApiKey, claudeDesktopModelDefs });
67
95
 
68
96
  // Point Codex and Claude Code at the adapter.
69
97
  ensureCodexConfig(ADAPTER_PORT);
@@ -73,6 +101,7 @@ try {
73
101
  port: ADAPTER_PORT,
74
102
  host: ADAPTER_HOST,
75
103
  gatewayApiKey: claudeDesktopApiKey,
104
+ modelIds: claudeDesktopModelIds(process.env, { modelDefs: claudeDesktopModelDefs }),
76
105
  });
77
106
  console.log(status("ok", `Configured Claude App gateway profile at ${result.baseUrl}`));
78
107
  console.log(formatClaudeDesktopApplyResult(result));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "type": "module",
6
6
  "bin": {
package/src/adapter.mjs CHANGED
@@ -567,6 +567,9 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
567
567
  || process.env.CCDX_CLAUDE_DESKTOP_API_KEY
568
568
  || process.env.CCDX_PROXY_API_KEY
569
569
  || "";
570
+ const claudeDesktopModelOptions = Array.isArray(options.claudeDesktopModelDefs)
571
+ ? { modelDefs: options.claudeDesktopModelDefs }
572
+ : {};
570
573
 
571
574
  const server = http.createServer((req, res) => {
572
575
  const pathname = requestPath(req.url);
@@ -648,7 +651,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
648
651
  if (req.method === "GET" && pathname === "/v1/models") {
649
652
  if (shouldServeClaudeDesktopModels(req, claudeDesktopApiKey)) {
650
653
  res.writeHead(200, { "Content-Type": "application/json" });
651
- res.end(JSON.stringify(claudeDesktopModelsResponse()));
654
+ res.end(JSON.stringify(claudeDesktopModelsResponse(process.env, claudeDesktopModelOptions)));
652
655
  return;
653
656
  }
654
657
  const abort = createRequestAbort(req, res);
@@ -688,7 +691,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
688
691
  try {
689
692
  const parsed = await readJsonBody(req);
690
693
  if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
691
- const { requestedModel, upstreamModel } = resolveAnthropicModel(parsed.model || "unknown");
694
+ const { requestedModel, upstreamModel } = resolveAnthropicModel(parsed.model || "unknown", process.env, claudeDesktopModelOptions);
692
695
  const modelNote = upstreamModel === requestedModel ? requestedModel : `${requestedModel} -> ${upstreamModel}`;
693
696
  console.log(status("info", `messages model=${modelNote} stream=${!!parsed.stream}`));
694
697
  const chatReq = anthropicToChat(parsed, { upstreamModel });
@@ -747,8 +750,18 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
747
750
  socket.destroy();
748
751
  });
749
752
 
750
- return new Promise((resolve) => {
753
+ return new Promise((resolve, reject) => {
754
+ const onListenError = (e) => {
755
+ if (e?.code === "EADDRINUSE") {
756
+ reject(new Error(`Adapter address http://${host}:${port} is already in use. Stop the existing codex-copilot-dx process or set ADAPTER_PORT to another port.`));
757
+ return;
758
+ }
759
+ reject(e);
760
+ };
761
+
762
+ server.once("error", onListenError);
751
763
  server.listen(port, host, () => {
764
+ server.off("error", onListenError);
752
765
  const actualPort = server.address()?.port || port;
753
766
  console.log(status("ok", `Adapter listening on http://${host}:${actualPort}`));
754
767
  resolve(server);
package/src/models.mjs CHANGED
@@ -1,4 +1,12 @@
1
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
+ },
2
10
  {
3
11
  id: "claude-sonnet-4.6",
4
12
  upstream: "claude-sonnet-4.6",
@@ -23,6 +31,9 @@ const DEFAULT_CLAUDE_DESKTOP_MODEL_DEFS = [
23
31
  maxInputTokens: 200000,
24
32
  maxOutputTokens: 8192,
25
33
  },
34
+ ];
35
+
36
+ const DEFAULT_CLAUDE_MODEL_ALIAS_DEFS = [
26
37
  {
27
38
  id: "claude-sonnet-4-6",
28
39
  upstream: "claude-sonnet-4.6",
@@ -73,18 +84,77 @@ export function parseModelAliasEnv(value) {
73
84
  return aliases;
74
85
  }
75
86
 
76
- export function claudeDesktopModelDefs(env = process.env) {
87
+ function cloneModelDefs(modelDefs) {
88
+ return modelDefs.map((model) => ({ ...model }));
89
+ }
90
+
91
+ function numericLimit(value, fallback) {
92
+ return Number.isFinite(value) && value > 0 ? value : fallback;
93
+ }
94
+
95
+ function hasMessagesEndpoint(model) {
96
+ const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
97
+ return endpoints.includes("/v1/messages") || endpoints.includes("/chat/completions");
98
+ }
99
+
100
+ function isClaudeCopilotModel(model) {
101
+ const id = String(model?.id || "").trim();
102
+ const vendor = String(model?.vendor || "").toLowerCase();
103
+ return id
104
+ && (id.startsWith("claude-") || vendor === "anthropic")
105
+ && model?.model_picker_enabled !== false
106
+ && hasMessagesEndpoint(model);
107
+ }
108
+
109
+ export function claudeDesktopModelDefsFromCopilotModels(models) {
110
+ const data = Array.isArray(models) ? models : models?.data;
111
+ if (!Array.isArray(data)) return [];
112
+
113
+ const defs = [];
114
+ const seen = new Set();
115
+ for (const model of data) {
116
+ if (!isClaudeCopilotModel(model)) continue;
117
+ const id = String(model.id || "").trim();
118
+ if (seen.has(id)) continue;
119
+ seen.add(id);
120
+
121
+ const limits = model.capabilities?.limits || {};
122
+ defs.push({
123
+ id,
124
+ upstream: id,
125
+ displayName: String(model.name || id),
126
+ createdAt: "2025-01-01T00:00:00Z",
127
+ maxInputTokens: numericLimit(limits.max_context_window_tokens, numericLimit(limits.max_prompt_tokens, 200000)),
128
+ maxOutputTokens: numericLimit(limits.max_output_tokens, 8192),
129
+ });
130
+ }
131
+
132
+ return defs;
133
+ }
134
+
135
+ export function claudeDesktopModelDefs(env = process.env, options = {}) {
77
136
  const custom = parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES);
78
- return custom.length ? custom : DEFAULT_CLAUDE_DESKTOP_MODEL_DEFS.map((model) => ({ ...model }));
137
+ if (custom.length) return custom;
138
+ if (Array.isArray(options.modelDefs) && options.modelDefs.length) return cloneModelDefs(options.modelDefs);
139
+ return cloneModelDefs(DEFAULT_CLAUDE_DESKTOP_MODEL_DEFS);
79
140
  }
80
141
 
81
- export function claudeDesktopModelIds(env = process.env) {
82
- return claudeDesktopModelDefs(env).map((model) => model.id);
142
+ export function claudeDesktopModelIds(env = process.env, options = {}) {
143
+ return claudeDesktopModelDefs(env, options).map((model) => model.id);
144
+ }
145
+
146
+ function claudeModelResolutionDefs(env = process.env, options = {}) {
147
+ const defs = claudeDesktopModelDefs(env, options);
148
+ if (parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES).length) return defs;
149
+
150
+ const upstreamIds = new Set(defs.map((model) => model.upstream));
151
+ const aliases = DEFAULT_CLAUDE_MODEL_ALIAS_DEFS.filter((alias) => upstreamIds.has(alias.upstream));
152
+ return [...defs, ...aliases];
83
153
  }
84
154
 
85
- export function resolveAnthropicModel(model, env = process.env) {
155
+ export function resolveAnthropicModel(model, env = process.env, options = {}) {
86
156
  const requestedModel = String(model || "");
87
- const match = claudeDesktopModelDefs(env).find((entry) => entry.id === requestedModel);
157
+ const match = claudeModelResolutionDefs(env, options).find((entry) => entry.id === requestedModel);
88
158
  return {
89
159
  requestedModel,
90
160
  upstreamModel: match?.upstream || requestedModel,
@@ -102,9 +172,9 @@ export function anthropicModelInfo(model) {
102
172
  };
103
173
  }
104
174
 
105
- export function claudeDesktopModelsResponse(env = process.env) {
175
+ export function claudeDesktopModelsResponse(env = process.env, options = {}) {
106
176
  return {
107
177
  object: "list",
108
- data: claudeDesktopModelDefs(env).map(anthropicModelInfo),
178
+ data: claudeDesktopModelDefs(env, options).map(anthropicModelInfo),
109
179
  };
110
180
  }