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 +24 -5
- package/package.json +1 -1
- package/src/adapter.mjs +35 -6
- package/src/copilot.mjs +29 -1
- package/src/models.mjs +17 -0
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
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
|
-
//
|
|
14
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 : [];
|