codex-copilot-dx 0.4.21 → 0.4.22

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
@@ -125,11 +125,20 @@ console.log(`
125
125
  Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot
126
126
  `);
127
127
 
128
- checkForUpdate({ currentVersion: LOCAL_VERSION }).then(({ latestVersion, updateAvailable }) => {
129
- if (!updateAvailable) return;
130
- console.log(`\n ${status("warn", `Update available: ${LOCAL_VERSION} -> ${latestVersion}`)}`);
131
- console.log(" npm install -g codex-copilot-dx@latest\n");
132
- });
128
+ async function printUpdateNotice() {
129
+ try {
130
+ const { latestVersion, updateAvailable } = await checkForUpdate({ currentVersion: LOCAL_VERSION });
131
+ if (!updateAvailable) return;
132
+ console.log(`\n ${status("warn", `Update available: ${LOCAL_VERSION} -> ${latestVersion}`)}`);
133
+ console.log(" npm install -g codex-copilot-dx@latest\n");
134
+ } catch {
135
+ // Never block startup on the update check.
136
+ }
137
+ }
138
+
139
+ // Await the update check up front so the notice is shown even on the
140
+ // "reuse existing adapter" path, which exits early below.
141
+ await printUpdateNotice();
133
142
 
134
143
  try {
135
144
  assertSafeAdapterHost(ADAPTER_HOST, process.env);
@@ -172,6 +181,16 @@ try {
172
181
  // Launch Codex when available.
173
182
  openCodex();
174
183
 
184
+ // Periodically refresh the model list + endpoint cache so a long-running
185
+ // adapter picks up newly released models without a restart.
186
+ const refreshIntervalMs = parseInt(process.env.CCDX_MODEL_REFRESH_INTERVAL_MS || String(30 * 60 * 1000), 10);
187
+ if (Number.isFinite(refreshIntervalMs) && refreshIntervalMs > 0) {
188
+ const timer = setInterval(() => {
189
+ refreshClaudeDesktopModelDefs().catch(() => {});
190
+ }, refreshIntervalMs);
191
+ if (typeof timer.unref === "function") timer.unref();
192
+ }
193
+
175
194
  console.log(`
176
195
  ${status("ok", "Ready, Claude Code, Claude App and Codex App are now ready to use")}
177
196
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.21",
3
+ "version": "0.4.22",
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
@@ -2,16 +2,20 @@ import http from "node:http";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { promisify } from "node:util";
4
4
  import * as zlib from "node:zlib";
5
- import { chatCompletions, listModels, responses as copilotResponses, responsesCompact as copilotResponsesCompact } from "./copilot.mjs";
5
+ import { chatCompletions, listModels, responses as copilotResponses, responsesCompact as copilotResponsesCompact, getCachedModelEndpoints } from "./copilot.mjs";
6
6
  import { webStreamLines } from "./stream.mjs";
7
7
  import { anthropicToChat, chatToAnthropic, streamAnthropicFromLines, countTokens } from "./anthropic.mjs";
8
- import { claudeDesktopModelsResponse, resolveAnthropicModel } from "./models.mjs";
8
+ import { claudeDesktopModelsResponse, resolveAnthropicModel, modelIsResponsesOnly, modelSupportsChatCompletions } from "./models.mjs";
9
9
  import { status } from "./status.mjs";
10
10
  import { recordAnthropicUsage, recordResponsesUsage } from "./usage.mjs";
11
11
  import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
12
12
 
13
- // Models that only support Responses API (not chat/completions)
14
- const RESPONSES_ONLY = new Set([
13
+ // Fallback list for models known to only support the Responses API, used when
14
+ // live model metadata (supported_endpoints) has not been cached yet.
15
+ const RESPONSES_ONLY_FALLBACK = new Set([
16
+ "gpt-5.6-luna",
17
+ "gpt-5.6-sol",
18
+ "gpt-5.6-terra",
15
19
  "gpt-5.5",
16
20
  "gpt-5.4",
17
21
  "gpt-5.4-mini",
@@ -19,6 +23,18 @@ const RESPONSES_ONLY = new Set([
19
23
  "gpt-5.2-codex",
20
24
  ]);
21
25
 
26
+ // Decide whether a model must be proxied straight to /responses (vs converted to
27
+ // /chat/completions). Prefer real endpoint metadata; fall back to the static list.
28
+ function isResponsesOnlyModel(model) {
29
+ const endpoints = getCachedModelEndpoints(model);
30
+ if (endpoints) {
31
+ const fakeModel = { supported_endpoints: endpoints };
32
+ if (modelSupportsChatCompletions(fakeModel)) return false;
33
+ if (modelIsResponsesOnly(fakeModel)) return true;
34
+ }
35
+ return RESPONSES_ONLY_FALLBACK.has(model);
36
+ }
37
+
22
38
  // Direct Copilot Responses API proxy.
23
39
 
24
40
  const RESPONSE_HISTORY_LIMIT = 64;
@@ -324,6 +340,19 @@ export function clearResponseHistoryForTests() {
324
340
  responseHistories.clear();
325
341
  }
326
342
 
343
+ // The GPT (gpt-5.6) Responses backend registers a built-in `image_gen` tool
344
+ // namespace. When the client also sends an image generation tool, the upstream
345
+ // rejects the request with "User-defined namespace 'image_gen' collides ...".
346
+ // Strip any built-in image tool (matched by type/name/namespace) before proxying.
347
+ function isBuiltinImageTool(tool) {
348
+ if (!tool || typeof tool !== "object") return false;
349
+ const candidates = [tool.type, tool.name, tool.namespace];
350
+ return candidates.some((v) => {
351
+ const s = String(v || "").toLowerCase();
352
+ return s === "image_gen" || s === "image_generation" || s.startsWith("image_gen");
353
+ });
354
+ }
355
+
327
356
  export function prepareResponsesRequest(reqBody) {
328
357
  const body = cloneJson(reqBody);
329
358
  const currentInputItems = responsesInputItems(body.input);
@@ -344,7 +373,7 @@ export function prepareResponsesRequest(reqBody) {
344
373
  delete body.previous_response_id;
345
374
  delete body.store;
346
375
  if (Array.isArray(body.tools)) {
347
- body.tools = body.tools.filter((tool) => tool?.type !== "image_generation");
376
+ body.tools = body.tools.filter((tool) => !isBuiltinImageTool(tool));
348
377
  if (!body.tools.length) delete body.tools;
349
378
  }
350
379
  stripInternalResponsesInputFields(body.input);
@@ -591,7 +620,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
591
620
  prepared.surface = "responses";
592
621
  const model = parsed.model || "unknown";
593
622
  console.log(status("info", `responses model=${model} stream=${!!parsed.stream}`));
594
- if (RESPONSES_ONLY.has(model)) {
623
+ if (isResponsesOnlyModel(model)) {
595
624
  await proxyCopilotResponses(prepared, req, res, copilotResponses, { signal: abort.signal });
596
625
  } else {
597
626
  const chatReq = responsesToChat(prepared.body);
package/src/copilot.mjs CHANGED
@@ -7,6 +7,30 @@ import { githubReauthMessage, githubTokenPath, importDiscoveredGithubToken } fro
7
7
 
8
8
  export const DEFAULT_API_BASE = "https://api.githubcopilot.com";
9
9
  let apiBase = DEFAULT_API_BASE;
10
+
11
+ // Cache of model id -> supported_endpoints, populated from listModels(). Used to
12
+ // route requests to /responses vs /chat/completions based on real model metadata.
13
+ const modelEndpointCache = new Map();
14
+
15
+ export function cacheModelEndpoints(models) {
16
+ const data = Array.isArray(models) ? models : models?.data;
17
+ if (!Array.isArray(data)) return;
18
+ for (const model of data) {
19
+ const id = String(model?.id || "").trim();
20
+ if (id && Array.isArray(model?.supported_endpoints)) {
21
+ modelEndpointCache.set(id, model.supported_endpoints);
22
+ }
23
+ }
24
+ }
25
+
26
+ // Returns the cached supported_endpoints for a model id, or null if unknown.
27
+ export function getCachedModelEndpoints(modelId) {
28
+ return modelEndpointCache.get(String(modelId || "").trim()) || null;
29
+ }
30
+
31
+ export function resetModelEndpointCacheForTests() {
32
+ modelEndpointCache.clear();
33
+ }
10
34
  const IMG_MAX_DIM = parseInt(process.env.CCDX_IMG_MAX_DIM || "2048", 10);
11
35
  const IMG_QUALITY = parseInt(process.env.CCDX_IMG_QUALITY || "85", 10);
12
36
  const IMG_MIN_BYTES = parseInt(process.env.CCDX_IMG_MIN_BYTES || "100000", 10);
@@ -486,7 +510,11 @@ export async function listModels({ signal, fetchImpl, retryOptions } = {}) {
486
510
  token, version: getVSCodeVersion(), initiator: "user", vision: false,
487
511
  });
488
512
  const resp = await fetchCopilotUpstream(`${getApiBase()}/models`, { headers, signal }, { fetchImpl, ...retryOptions });
489
- return { status: resp.status, body: await resp.text() };
513
+ const body = await resp.text();
514
+ if (resp.ok) {
515
+ try { cacheModelEndpoints(JSON.parse(body)); } catch {}
516
+ }
517
+ return { status: resp.status, body };
490
518
  }
491
519
 
492
520
  // Responses-only models go directly to Copilot's /responses endpoint.
package/src/models.mjs CHANGED
@@ -102,6 +102,23 @@ function hasOpenAIEndpoint(model) {
102
102
  return endpoints.includes("/responses") || endpoints.includes("/v1/responses") || endpoints.includes("/chat/completions");
103
103
  }
104
104
 
105
+ // Endpoint-based routing helpers. A model is "responses-only" when it exposes a
106
+ // /responses endpoint but does NOT accept /chat/completions. These read the real
107
+ // model metadata so new models (e.g. gpt-5.6-*) route correctly without a hardcoded list.
108
+ export function modelEndpoints(model) {
109
+ return Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
110
+ }
111
+
112
+ export function modelSupportsChatCompletions(model) {
113
+ return modelEndpoints(model).includes("/chat/completions");
114
+ }
115
+
116
+ export function modelIsResponsesOnly(model) {
117
+ const endpoints = modelEndpoints(model);
118
+ const hasResponses = endpoints.includes("/responses") || endpoints.includes("/v1/responses");
119
+ return hasResponses && !endpoints.includes("/chat/completions");
120
+ }
121
+
105
122
  function copilotModelData(models) {
106
123
  const data = Array.isArray(models) ? models : models?.data;
107
124
  return Array.isArray(data) ? data : [];