codex-copilot-dx 0.7.1 → 0.7.2

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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # codex-copilot-dx
2
2
 
3
- Use Codex Desktop or the ChatGPT app with GPT models provided by your **GitHub Copilot** subscription.
3
+ Use Codex App with GPT models provided by your **GitHub Copilot** subscription.
4
4
 
5
- CCDX is a local compatibility adapter. It configures Codex-compatible desktop clients to use an OpenAI Responses API on loopback while GitHub authentication, model discovery, and inference stay backed by GitHub Copilot.
5
+ CCDX is a local compatibility adapter for Codex App. It configures Codex to use an OpenAI Responses API on loopback while GitHub authentication, model discovery, and inference stay backed by GitHub Copilot.
6
6
 
7
7
  ## How it works
8
8
 
@@ -14,7 +14,7 @@ One in-process adapter listens on `127.0.0.1:2026` by default and exposes:
14
14
 
15
15
  Responses-only models and compaction use Copilot's Responses transport directly. For GPT models that advertise only Chat Completions, CCDX converts the supported text, image, and function-tool subset and rejects incompatible request shapes explicitly. Both streaming SSE and non-streaming responses are supported.
16
16
 
17
- The adapter preserves Codex and ChatGPT app behavior around response history, encrypted reasoning state, function/custom tools, images, compaction, cancellation, retries, and usage metadata. Model routing follows the live Copilot catalog instead of a hard-coded GPT model list.
17
+ The adapter preserves Codex App behavior around response history, encrypted reasoning state, function/custom tools, images, compaction, cancellation, retries, and usage metadata. Model routing follows the live Copilot catalog instead of a hard-coded GPT model list.
18
18
 
19
19
  ### Fast and Standard modes
20
20
 
@@ -22,11 +22,17 @@ Codex can select Standard or Fast without changing CCDX configuration. When the
22
22
 
23
23
  Codex Auto-review uses the hidden `codex-auto-review` model ID. CCDX maps it to Copilot's `gpt-5.5` Responses model by default, independently of the interactive Fast/Standard selection, and logs both model IDs when the mapping is used. The review target can be changed interactively without reinstalling or restarting CCDX.
24
24
 
25
+ ### GPT-6 model catalog
26
+
27
+ Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It makes `gpt-6-astra` visible only when the live Copilot catalog advertises that exact model as enabled, selectable, OpenAI-owned, and Responses-capable.
28
+
29
+ CCDX 0.7.2 exposes only the verified GPT-6 Standard path and removes its unverified speed-tier metadata. Codex Ultra remains available because the client maps it to the model's supported multi-agent reasoning effort. GPT-5.6 Sol Fast remains unchanged. The bundled catalog is matched to the Codex client version, cached by application binary identity, and reloaded automatically after an App update.
30
+
25
31
  ## Prerequisites
26
32
 
27
33
  - GitHub Copilot subscription (Individual, Business, or Enterprise)
28
34
  - Node.js 22.15 or newer, for built-in Zstandard request decompression
29
- - Codex Desktop or the ChatGPT app on macOS
35
+ - Codex App on macOS
30
36
 
31
37
  ## Install and start
32
38
 
@@ -49,7 +55,7 @@ On a normal launch, CCDX:
49
55
  2. Reuses a compatible running adapter when available; otherwise it reuses a compatible local Copilot credential or starts GitHub Device Flow when authentication is required.
50
56
  3. Refreshes Copilot model metadata and listens on the configured loopback address.
51
57
  4. Updates `~/.codex/config.toml` to use the local `/v1` endpoint while preserving unrelated Codex settings.
52
- 5. Attempts to open Codex Desktop or the ChatGPT app on macOS unless `CCDX_AUTO_LAUNCH` disables it.
58
+ 5. Attempts to open Codex App on macOS unless `CCDX_AUTO_LAUNCH` disables it.
53
59
 
54
60
  The `OPENAI_API_KEY=dummy` value written into the Codex shell environment is only a client-side placeholder. It is not a GitHub credential. CCDX exchanges the saved GitHub OAuth credential for short-lived Copilot service tokens internally.
55
61
 
@@ -227,7 +233,7 @@ ccdx
227
233
  | `CCDX_IMG_MAX_INPUT_PIXELS` | `40000000` | Maximum decoded pixels accepted for one image |
228
234
  | `CCDX_IMG_CACHE_MAX_BYTES` | `67108864` | Process-local byte ceiling for cached image transforms; set to `0` to disable the cache |
229
235
  | `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
230
- | `CCDX_AUTO_LAUNCH` | enabled | Set to `0`, `false`, `no`, or `off` to start without opening Codex or ChatGPT |
236
+ | `CCDX_AUTO_LAUNCH` | enabled | Set to `0`, `false`, `no`, or `off` to start without opening Codex App |
231
237
  | `CCDX_GITHUB_TOKEN` | unset | Explicit GitHub Copilot OAuth token to validate and import before Device Flow |
232
238
  | `CCDX_GITHUB_TOKEN_PATH` | unset | Explicit file containing a GitHub Copilot OAuth token |
233
239
  | `CCDX_GITHUB_TOKEN_PATHS` | unset | Multiple token files separated by the platform path delimiter (`:` on macOS/Linux, `;` on Windows) |
@@ -277,7 +283,7 @@ For `/v1/responses/compact`, CCDX sends one terminal `compaction_trigger`, valid
277
283
 
278
284
  Successful streaming responses are accepted only when the upstream body is SSE and reaches its protocol terminal event. Unexpected EOF becomes a protocol-native stream error, and repeated blank tool-argument deltas are stopped per tool call. Safe quota, retry, model, trace, and upstream request-ID metadata are forwarded; cookies, authorization, body-length, and encoding headers are not.
279
285
 
280
- Newer Codex and ChatGPT clients can advertise an `image_gen` namespace that already exists upstream. CCDX removes that exact conflicting client tool before forwarding and retries once only when Copilot explicitly reports an image namespace collision. Image input and screenshot optimization remain enabled.
286
+ Newer Codex App builds can advertise an `image_gen` namespace that already exists upstream. CCDX removes that exact conflicting client tool before forwarding and retries once only when Copilot explicitly reports an image namespace collision. Image input and screenshot optimization remain enabled.
281
287
 
282
288
  ## Development
283
289
 
package/bin/cli.mjs CHANGED
@@ -21,6 +21,7 @@ import { fetchLiveCopilotModels, formatLiveCopilotModels } from "../src/cli-mode
21
21
  import { runAuthCommand } from "../src/cli-auth.mjs";
22
22
  import { createProfileRuntime } from "../src/profile-runtime.mjs";
23
23
  import { createProfileModelRuntime } from "../src/profile-model-runtime.mjs";
24
+ import { createCodexModelCatalog } from "../src/codex-model-catalog.mjs";
24
25
 
25
26
  const LOCAL_VERSION = localPackageVersion();
26
27
  const CLI_NAME = cliCommandName();
@@ -201,7 +202,7 @@ if (CLI.command === "doctor") {
201
202
 
202
203
  console.log(`
203
204
  ${CLI_BANNER}
204
- Use Codex Desktop and ChatGPT App with GitHub Copilot
205
+ Use Codex App with GitHub Copilot
205
206
  `);
206
207
 
207
208
  async function printUpdateNotice() {
@@ -242,9 +243,11 @@ try {
242
243
 
243
244
  // Start the in-process adapter.
244
245
  const terminalAnimationTheme = terminalAnimationPreference().theme;
246
+ const codexModelCatalog = createCodexModelCatalog();
245
247
  activeServer = await startAdapter(ADAPTER_PORT, ADAPTER_HOST, {
246
248
  autoReviewModelResolver: () => autoReviewModelPreference().model,
247
249
  codexClient: profileRuntime.codexClient,
250
+ codexModelCatalog,
248
251
  codexModelRegistry: profileModels.codexRegistry,
249
252
  showRequestId: CLI.showRequestId,
250
253
  upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
@@ -253,7 +256,7 @@ try {
253
256
  terminalAnimationTheme,
254
257
  });
255
258
 
256
- // Point Codex and compatible ChatGPT App builds at the adapter.
259
+ // Point Codex App at the adapter.
257
260
  ensureCodexConfig(ADAPTER_PORT, { host: ADAPTER_HOST });
258
261
 
259
262
  // Launch Codex when available.
@@ -270,7 +273,7 @@ try {
270
273
  }
271
274
 
272
275
  console.log(`
273
- ${status("ok", "Ready, Codex App and ChatGPT App are ready to use")}
276
+ ${status("ok", "Ready, Codex App is ready to use")}
274
277
 
275
278
  Adapter: ${adapterBaseUrl(ADAPTER_HOST, ADAPTER_PORT)}
276
279
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.7.1",
4
- "description": "Use Codex Desktop and ChatGPT App with GitHub Copilot",
3
+ "version": "0.7.2",
4
+ "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
7
7
  "bin": {
package/src/adapter.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import http from "node:http";
2
2
  import { defaultCopilotClient } from "./copilot.mjs";
3
+ import { buildCodexModelResponse } from "./codex-model-catalog.mjs";
3
4
  import { isValidModelList } from "./model-cache.mjs";
4
5
  import {
5
6
  ADAPTER_STATUS_PATH,
@@ -78,13 +79,58 @@ function modelListCanUseLastKnownGood(statusCode) {
78
79
  return [408, 425, 429].includes(statusCode) || statusCode >= 500;
79
80
  }
80
81
 
81
- function sendLastKnownGoodModels(res, modelRegistry) {
82
+ function modelClientVersion(reqUrl) {
83
+ try {
84
+ return String(new URL(reqUrl || "/", "http://localhost").searchParams.get("client_version") || "").trim();
85
+ } catch {
86
+ return "";
87
+ }
88
+ }
89
+
90
+ async function sendModelList(res, models, {
91
+ codexModelCatalog,
92
+ clientVersion = "",
93
+ headers = {},
94
+ rawBody,
95
+ statusCode = 200,
96
+ } = {}) {
97
+ let body = rawBody || JSON.stringify(models);
98
+ if (clientVersion && typeof codexModelCatalog?.load === "function") {
99
+ let response = null;
100
+ try {
101
+ const codexCatalog = await codexModelCatalog.load({ clientVersion });
102
+ response = buildCodexModelResponse({ copilotModels: models, codexCatalog });
103
+ } catch {}
104
+ if (!response) {
105
+ if (res.destroyed || res.writableEnded) return false;
106
+ res.writeHead(503, {
107
+ "Content-Type": "application/json",
108
+ "Retry-After": "5",
109
+ ...headers,
110
+ });
111
+ res.end(JSON.stringify({
112
+ error: {
113
+ message: "The Codex App model catalog is temporarily unavailable",
114
+ type: "server_error",
115
+ code: "ccdx_codex_model_catalog_unavailable",
116
+ },
117
+ }));
118
+ return true;
119
+ }
120
+ body = JSON.stringify(response);
121
+ }
122
+ if (res.destroyed || res.writableEnded) return false;
123
+ res.writeHead(statusCode, { "Content-Type": "application/json", ...headers });
124
+ res.end(body);
125
+ return true;
126
+ }
127
+
128
+ async function sendLastKnownGoodModels(res, modelRegistry, options = {}) {
82
129
  if (!isValidModelList(modelRegistry?.models) || res.destroyed || res.writableEnded) return false;
83
- res.writeHead(200, {
84
- "Content-Type": "application/json",
85
- "X-CCDX-Model-Source": "last-known-good",
130
+ await sendModelList(res, modelRegistry.models, {
131
+ ...options,
132
+ headers: { "X-CCDX-Model-Source": "last-known-good" },
86
133
  });
87
- res.end(JSON.stringify(modelRegistry.models));
88
134
  return true;
89
135
  }
90
136
 
@@ -100,6 +146,7 @@ export function createAdapterHandler(options = {}) {
100
146
  const responsesFn = options.responsesFn || codexClient.responses;
101
147
  const responsesCompactFn = options.responsesCompactFn || codexClient.responsesCompact;
102
148
  const listModelsFn = options.listModelsFn || codexClient.listModels;
149
+ const codexModelCatalog = options.codexModelCatalog;
103
150
  const getCachedModelEndpointsFn = options.getCachedModelEndpointsFn
104
151
  || codexClient.getCachedModelEndpoints;
105
152
  const codexModelRegistry = options.codexModelRegistry || options.modelRegistry;
@@ -176,10 +223,12 @@ export function createAdapterHandler(options = {}) {
176
223
  }
177
224
 
178
225
  if (req.method === "GET" && pathname === "/v1/models") {
226
+ const clientVersion = modelClientVersion(req.url);
179
227
  const abort = createRequestAbort(req, res);
180
228
  abort.setTimeout(upstreamTimeoutMs);
181
229
  listModelsFn({ signal: abort.signal })
182
- .then(({ status, body }) => {
230
+ .then(async ({ status, body }) => {
231
+ abort.clearTimeout();
183
232
  if (status >= 200 && status < 300) {
184
233
  let models;
185
234
  try { models = JSON.parse(body); } catch {}
@@ -187,21 +236,26 @@ export function createAdapterHandler(options = {}) {
187
236
  if (codexModelRegistry) {
188
237
  codexModelRegistry.models = models;
189
238
  }
190
- res.writeHead(status, { "Content-Type": "application/json" });
191
- res.end(body);
239
+ await sendModelList(res, models, {
240
+ codexModelCatalog,
241
+ clientVersion,
242
+ rawBody: body,
243
+ statusCode: status,
244
+ });
192
245
  return;
193
246
  }
194
- if (sendLastKnownGoodModels(res, codexModelRegistry)) return;
247
+ if (await sendLastKnownGoodModels(res, codexModelRegistry, { codexModelCatalog, clientVersion })) return;
195
248
  sendJsonError(res, httpError("Copilot models response contained no valid models", 502), 502);
196
249
  return;
197
250
  }
198
- if (modelListCanUseLastKnownGood(status) && sendLastKnownGoodModels(res, codexModelRegistry)) return;
251
+ if (modelListCanUseLastKnownGood(status)
252
+ && await sendLastKnownGoodModels(res, codexModelRegistry, { codexModelCatalog, clientVersion })) return;
199
253
  res.writeHead(status, { "Content-Type": "application/json" });
200
254
  res.end(body);
201
255
  })
202
- .catch((e) => {
256
+ .catch(async (e) => {
203
257
  logRequestFailure("Models", e, abort);
204
- if (sendLastKnownGoodModels(res, codexModelRegistry)) return;
258
+ if (await sendLastKnownGoodModels(res, codexModelRegistry, { codexModelCatalog, clientVersion })) return;
205
259
  if (!res.destroyed && !res.writableEnded) {
206
260
  if (!res.headersSent) res.writeHead(e?.statusCode || 502);
207
261
  res.end(JSON.stringify({ error: e.message }));
@@ -273,7 +273,7 @@ export function cliCommandName() {
273
273
 
274
274
  function topicHelp(name, topic) {
275
275
  const sections = {
276
- start: `Usage:\n ${name} [start] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex configuration, and attempts to open Codex or ChatGPT when auto-launch is enabled and supported.`,
276
+ start: `Usage:\n ${name} [start] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex configuration, and attempts to open Codex App when auto-launch is enabled and supported.`,
277
277
  auth: `Usage:\n ${name} auth status [--online] [--format table|plain]\n\nShows the saved GitHub Copilot account without exposing credentials. --online also verifies its entitlement and model catalog.`,
278
278
  "auth status": `Usage:\n ${name} auth status [--online] [--format table|plain]\n\nShows the saved GitHub Copilot account without exposing credentials. --online also verifies its entitlement and model catalog. Interactive terminals use a table by default.`,
279
279
  doctor: `Usage:\n ${name} doctor [--online] [--compat]\n\nChecks the local credential, Codex configuration, and adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage.`,
@@ -0,0 +1,261 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { CODEX_GPT6_MODEL } from "./models.mjs";
6
+
7
+ export { CODEX_GPT6_MODEL };
8
+
9
+ export const CODEX_APP_BINARY_PATHS = [
10
+ "/Applications/Codex.app/Contents/Resources/codex",
11
+ "/Applications/ChatGPT.app/Contents/Resources/codex",
12
+ path.join(os.homedir(), "Applications", "Codex.app", "Contents", "Resources", "codex"),
13
+ path.join(os.homedir(), "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
14
+ ];
15
+ export const CODEX_MODEL_CATALOG_TIMEOUT_MS = 5_000;
16
+ export const CODEX_MODEL_CATALOG_MAX_BUFFER = 8 * 1024 * 1024;
17
+ export const CODEX_MODEL_CATALOG_FAILURE_BACKOFF_MS = 5_000;
18
+
19
+ function objectRecord(value) {
20
+ return value !== null && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ function codexCatalogIsValid(catalog) {
24
+ if (!objectRecord(catalog) || !Array.isArray(catalog.models) || catalog.models.length === 0) return false;
25
+ const slugs = new Set();
26
+ for (const model of catalog.models) {
27
+ const slug = String(model?.slug || "").trim();
28
+ if (!objectRecord(model) || !slug || slugs.has(slug)) return false;
29
+ slugs.add(slug);
30
+ }
31
+ return true;
32
+ }
33
+
34
+ function copilotModelData(models) {
35
+ if (!objectRecord(models) || !Array.isArray(models.data) || models.data.length === 0) return null;
36
+ return models.data;
37
+ }
38
+
39
+ function supportsResponses(model) {
40
+ const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
41
+ return endpoints.includes("/responses") || endpoints.includes("/v1/responses");
42
+ }
43
+
44
+ function isEligibleOpenAIModel(model, id) {
45
+ const vendor = String(model?.vendor || model?.owned_by || "").trim().toLowerCase();
46
+ return String(model?.id || "").trim() === id
47
+ && vendor === "openai"
48
+ && String(model?.policy?.state || "").trim().toLowerCase() === "enabled"
49
+ && model?.model_picker_enabled === true
50
+ && supportsResponses(model);
51
+ }
52
+
53
+ export function isEligibleCopilotGpt6(copilotModels) {
54
+ const data = copilotModelData(copilotModels);
55
+ if (!data) return false;
56
+ const matches = data.filter((model) => String(model?.id || "").trim() === CODEX_GPT6_MODEL);
57
+ return matches.length === 1 && isEligibleOpenAIModel(matches[0], CODEX_GPT6_MODEL);
58
+ }
59
+
60
+ export function buildCodexModelResponse({ copilotModels, codexCatalog } = {}) {
61
+ if (!copilotModelData(copilotModels) || !codexCatalogIsValid(codexCatalog)) return null;
62
+
63
+ const exposeGpt6 = isEligibleCopilotGpt6(copilotModels);
64
+ const models = codexCatalog.models.map((model) => {
65
+ if (model.slug !== CODEX_GPT6_MODEL) return model;
66
+ if (!exposeGpt6) return { ...model, visibility: "hide" };
67
+ const patched = {
68
+ ...model,
69
+ visibility: "list",
70
+ additional_speed_tiers: [],
71
+ service_tiers: [],
72
+ };
73
+ delete patched.default_service_tier;
74
+ return patched;
75
+ });
76
+
77
+ return { ...copilotModels, object: copilotModels.object || "list", models };
78
+ }
79
+
80
+ function binaryCacheKey(binaryPath, stats) {
81
+ return [
82
+ binaryPath,
83
+ stats.dev,
84
+ stats.ino,
85
+ stats.size,
86
+ stats.mtimeMs,
87
+ stats.ctimeMs,
88
+ ].map((part) => String(part ?? "")).join(":");
89
+ }
90
+
91
+ async function installedCodexBinaries(binaryPaths, statFn) {
92
+ const installed = [];
93
+ for (const binaryPath of binaryPaths) {
94
+ let stats;
95
+ try {
96
+ stats = await statFn(binaryPath);
97
+ } catch {
98
+ continue;
99
+ }
100
+ if (stats && typeof stats.isFile === "function" && stats.isFile()) {
101
+ installed.push({ binaryPath, key: binaryCacheKey(binaryPath, stats) });
102
+ }
103
+ }
104
+ return installed;
105
+ }
106
+
107
+ function runCodexCommand(execFileFn, binaryPath, args, options) {
108
+ return new Promise((resolve, reject) => {
109
+ try {
110
+ execFileFn(binaryPath, args, options, (error, stdout) => {
111
+ if (error) reject(error);
112
+ else resolve(stdout);
113
+ });
114
+ } catch (error) {
115
+ reject(error);
116
+ }
117
+ });
118
+ }
119
+
120
+ function parsedCodexVersion(stdout) {
121
+ const match = /^(?:codex-cli|codex)\s+(\S+)$/.exec(String(stdout).trim());
122
+ return match?.[1] || null;
123
+ }
124
+
125
+ function wholeSemver(value) {
126
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(String(value || "").trim());
127
+ if (!match) return null;
128
+ if (match[4]?.split(".").some((part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith("0"))) {
129
+ return null;
130
+ }
131
+ return `${match[1]}.${match[2]}.${match[3]}`;
132
+ }
133
+
134
+ function normalizedBackoff(value) {
135
+ return Number.isFinite(value) && value >= 0 ? value : CODEX_MODEL_CATALOG_FAILURE_BACKOFF_MS;
136
+ }
137
+
138
+ export function createCodexModelCatalog({
139
+ binaryPath,
140
+ execFileFn = execFile,
141
+ statFn = fs.promises.stat,
142
+ timeoutMs = CODEX_MODEL_CATALOG_TIMEOUT_MS,
143
+ maxBuffer = CODEX_MODEL_CATALOG_MAX_BUFFER,
144
+ failureBackoffMs = CODEX_MODEL_CATALOG_FAILURE_BACKOFF_MS,
145
+ now = Date.now,
146
+ } = {}) {
147
+ const binaryPaths = binaryPath === undefined
148
+ ? CODEX_APP_BINARY_PATHS
149
+ : [binaryPath].filter((candidate) => typeof candidate === "string" && path.isAbsolute(candidate));
150
+ const commandOptions = {
151
+ encoding: "utf8",
152
+ maxBuffer,
153
+ timeout: timeoutMs,
154
+ windowsHide: true,
155
+ };
156
+ const backoffMs = normalizedBackoff(failureBackoffMs);
157
+ const versionCache = new Map();
158
+ const versionFailures = new Map();
159
+ const versionPending = new Map();
160
+ const catalogCache = new Map();
161
+ const catalogFailures = new Map();
162
+ const catalogPending = new Map();
163
+ let activeBinaryKeys = new Set();
164
+
165
+ function retainCurrentEntries(map) {
166
+ for (const key of map.keys()) {
167
+ if (!activeBinaryKeys.has(key)) map.delete(key);
168
+ }
169
+ }
170
+
171
+ async function probeVersion(candidate) {
172
+ if (versionCache.has(candidate.key)) return versionCache.get(candidate.key);
173
+ const failure = versionFailures.get(candidate.key);
174
+ if (failure && now() < failure.retryAt) return null;
175
+ if (versionPending.has(candidate.key)) return versionPending.get(candidate.key);
176
+
177
+ const promise = (async () => {
178
+ try {
179
+ const stdout = await runCodexCommand(execFileFn, candidate.binaryPath, ["--version"], commandOptions);
180
+ const version = wholeSemver(parsedCodexVersion(stdout));
181
+ if (!version) throw new Error("Invalid Codex version output");
182
+ if (activeBinaryKeys.has(candidate.key)) {
183
+ versionCache.set(candidate.key, version);
184
+ versionFailures.delete(candidate.key);
185
+ }
186
+ return version;
187
+ } catch {
188
+ if (activeBinaryKeys.has(candidate.key)) {
189
+ versionFailures.set(candidate.key, { retryAt: now() + backoffMs });
190
+ }
191
+ return null;
192
+ }
193
+ })();
194
+ versionPending.set(candidate.key, promise);
195
+ try {
196
+ return await promise;
197
+ } finally {
198
+ if (versionPending.get(candidate.key) === promise) versionPending.delete(candidate.key);
199
+ }
200
+ }
201
+
202
+ async function selectBinary(clientVersion) {
203
+ const installed = await installedCodexBinaries(binaryPaths, statFn);
204
+ activeBinaryKeys = new Set(installed.map((candidate) => candidate.key));
205
+ for (const map of [versionCache, versionFailures, catalogCache, catalogFailures]) {
206
+ retainCurrentEntries(map);
207
+ }
208
+ if (installed.length === 0) return null;
209
+ const rawVersion = String(clientVersion || "").trim();
210
+ if (!rawVersion) return installed[0];
211
+ const expectedVersion = wholeSemver(rawVersion);
212
+ if (!expectedVersion) return null;
213
+ for (const candidate of installed) {
214
+ if (await probeVersion(candidate) === expectedVersion) return candidate;
215
+ }
216
+ return null;
217
+ }
218
+
219
+ async function loadCatalog(candidate) {
220
+ if (catalogCache.has(candidate.key)) return catalogCache.get(candidate.key);
221
+ const failure = catalogFailures.get(candidate.key);
222
+ if (failure && now() < failure.retryAt) return null;
223
+ if (catalogPending.has(candidate.key)) return catalogPending.get(candidate.key);
224
+
225
+ const promise = (async () => {
226
+ try {
227
+ const stdout = await runCodexCommand(
228
+ execFileFn,
229
+ candidate.binaryPath,
230
+ ["debug", "models", "--bundled"],
231
+ commandOptions,
232
+ );
233
+ const catalog = JSON.parse(String(stdout));
234
+ if (!codexCatalogIsValid(catalog)) throw new Error("Invalid Codex model catalog");
235
+ if (activeBinaryKeys.has(candidate.key)) {
236
+ catalogCache.set(candidate.key, catalog);
237
+ catalogFailures.delete(candidate.key);
238
+ }
239
+ return catalog;
240
+ } catch {
241
+ if (activeBinaryKeys.has(candidate.key)) {
242
+ catalogFailures.set(candidate.key, { retryAt: now() + backoffMs });
243
+ }
244
+ return null;
245
+ }
246
+ })();
247
+ catalogPending.set(candidate.key, promise);
248
+ try {
249
+ return await promise;
250
+ } finally {
251
+ if (catalogPending.get(candidate.key) === promise) catalogPending.delete(candidate.key);
252
+ }
253
+ }
254
+
255
+ async function load({ clientVersion = "" } = {}) {
256
+ const candidate = await selectBinary(clientVersion);
257
+ return candidate ? loadCatalog(candidate) : null;
258
+ }
259
+
260
+ return { load };
261
+ }
package/src/launcher.mjs CHANGED
@@ -3,8 +3,7 @@ import { status } from "./status.mjs";
3
3
 
4
4
  export { ensureAuth } from "./auth.mjs";
5
5
 
6
- // Prefer the Codex bundle identifier, then fall back to the known app paths.
7
- // This supports both Codex.app and ChatGPT.app regardless of installation path.
6
+ // Prefer the Codex bundle identifier, then fall back to its known app paths.
8
7
  const CODEX_BUNDLE_ID = "com.openai.codex";
9
8
  const APP_PATH_CANDIDATES = [
10
9
  "/Applications/Codex.app",
@@ -40,12 +39,12 @@ export function launchAttempts() {
40
39
  export async function openCodex({ env = process.env, spawnImpl = spawn, platform = process.platform } = {}) {
41
40
  if (platform !== "darwin") {
42
41
  console.log(status("warn", "Auto-launch is only supported on macOS"));
43
- console.log(status("info", "Open Codex or the ChatGPT app manually; it will connect to the adapter."));
42
+ console.log(status("info", "Open Codex App manually; it will connect to the adapter."));
44
43
  return false;
45
44
  }
46
45
 
47
46
  if (!autoLaunchEnabled(env)) {
48
- console.log(status("info", "Auto-launch disabled (CCDX_AUTO_LAUNCH=0); open Codex or the ChatGPT app manually."));
47
+ console.log(status("info", "Auto-launch disabled (CCDX_AUTO_LAUNCH=0); open Codex App manually."));
49
48
  return false;
50
49
  }
51
50
 
@@ -57,7 +56,7 @@ export async function openCodex({ env = process.env, spawnImpl = spawn, platform
57
56
  }
58
57
 
59
58
  console.log(status("warn", "Could not launch the Codex app (checked com.openai.codex, Codex.app, ChatGPT.app)"));
60
- console.log(status("info", "Open Codex or the ChatGPT app manually; it will connect to the adapter."));
59
+ console.log(status("info", "Open Codex App manually; it will connect to the adapter."));
61
60
  console.log(status("info", "Download it from https://openai.com/codex"));
62
61
  return false;
63
62
  }
package/src/models.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  export const CODEX_AUTO_REVIEW_MODEL = "codex-auto-review";
2
2
  export const DEFAULT_CODEX_AUTO_REVIEW_MODEL = "gpt-5.5";
3
+ export const CODEX_GPT6_MODEL = "gpt-6-astra";
3
4
 
4
5
  function hasOpenAIEndpoint(model) {
5
6
  const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
@@ -12,6 +12,7 @@ import {
12
12
  writeOrDrain,
13
13
  } from "./http-transport.mjs";
14
14
  import {
15
+ CODEX_GPT6_MODEL,
15
16
  CODEX_AUTO_REVIEW_MODEL,
16
17
  modelIsResponsesOnly,
17
18
  modelSupportsChatCompletions,
@@ -87,6 +88,14 @@ function resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver) {
87
88
  return resolveOpenAIModel(model, openAIModelEnv, options);
88
89
  }
89
90
 
91
+ function stripUnsupportedGpt6ServiceTier(body, requestedModel, upstreamModel) {
92
+ if (requestedModel === CODEX_GPT6_MODEL
93
+ && upstreamModel === requestedModel
94
+ && body.service_tier === "priority") {
95
+ delete body.service_tier;
96
+ }
97
+ }
98
+
90
99
  export function createResponsesHandler(options) {
91
100
  const {
92
101
  acquireRequest,
@@ -216,6 +225,7 @@ export function createResponsesHandler(options) {
216
225
  upstreamModel = priorityTierModel;
217
226
  delete prepared.body.service_tier;
218
227
  }
228
+ stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel);
219
229
  if (requestedModel === CODEX_AUTO_REVIEW_MODEL) delete prepared.body.service_tier;
220
230
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
221
231
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
@@ -465,6 +475,7 @@ export function createResponsesCompactHandler(options) {
465
475
  upstreamModel = priorityTierModel;
466
476
  delete prepared.body.service_tier;
467
477
  }
478
+ stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel);
468
479
  if (requestedModel === CODEX_AUTO_REVIEW_MODEL) delete prepared.body.service_tier;
469
480
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
470
481
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;