github-router 0.3.11 → 0.3.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/dist/main.js CHANGED
@@ -6,6 +6,8 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { randomBytes, randomUUID } from "node:crypto";
8
8
  import process$1 from "node:process";
9
+ import fs$1 from "node:fs";
10
+ import { Writable } from "node:stream";
9
11
  import { execFileSync, spawn } from "node:child_process";
10
12
  import { serve } from "srvx";
11
13
  import { getProxyForUrl } from "proxy-from-env";
@@ -19,9 +21,11 @@ import clipboard from "clipboardy";
19
21
  //#region src/lib/paths.ts
20
22
  const APP_DIR = path.join(os.homedir(), ".local", "share", "github-router");
21
23
  const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
24
+ const ERROR_LOG_PATH = path.join(APP_DIR, "error.log");
22
25
  const PATHS = {
23
26
  APP_DIR,
24
- GITHUB_TOKEN_PATH
27
+ GITHUB_TOKEN_PATH,
28
+ ERROR_LOG_PATH
25
29
  };
26
30
  async function ensurePaths() {
27
31
  await fs.mkdir(PATHS.APP_DIR, { recursive: true });
@@ -223,19 +227,68 @@ function filterBetaHeader(value) {
223
227
  return value.split(",").map((v) => v.trim()).filter((v) => v && ALLOWED_BETA_PREFIXES.some((prefix) => v.startsWith(prefix))).join(",") || void 0;
224
228
  }
225
229
  /**
230
+ * Normalize a model ID for fuzzy comparison: lowercase, replace dots with
231
+ * dashes, insert dash at letter→digit boundaries, and collapse repeated
232
+ * dashes. E.g. "gpt5.3-codex" → "gpt-5-3-codex", "GPT-5.3-Codex" → "gpt-5-3-codex".
233
+ */
234
+ function normalizeModelId(id) {
235
+ return id.toLowerCase().replace(/\./g, "-").replace(/([a-z])(\d)/g, "$1-$2").replace(/-{2,}/g, "-");
236
+ }
237
+ /**
226
238
  * Resolve a model name to the best available variant in the Copilot model list.
227
- * Prefers the 1M context variant for opus models.
239
+ *
240
+ * Resolution cascade:
241
+ * 1. Exact match
242
+ * 2. Case-insensitive match
243
+ * 3. Family preference (opus→1m, codex→highest version)
244
+ * 4. Normalized match (dots→dashes, letter-digit boundaries)
245
+ * 5. Return as-is with a warning
228
246
  */
229
247
  function resolveModel(modelId) {
230
248
  const models = state.models?.data;
231
249
  if (!models) return modelId;
232
250
  if (models.some((m) => m.id === modelId)) return modelId;
233
- if (modelId.toLowerCase().includes("opus")) {
251
+ const lower = modelId.toLowerCase();
252
+ const ciMatch = models.find((m) => m.id.toLowerCase() === lower);
253
+ if (ciMatch) return ciMatch.id;
254
+ if (lower.includes("opus")) {
234
255
  const oneM = models.find((m) => m.id.includes("opus") && m.id.endsWith("-1m"));
235
256
  if (oneM) return oneM.id;
236
257
  }
258
+ if (lower.includes("codex")) {
259
+ const codexModels = models.filter((m) => m.id.includes("codex") && !m.id.includes("mini"));
260
+ if (codexModels.length > 0) {
261
+ codexModels.sort((a, b) => b.id.localeCompare(a.id));
262
+ return codexModels[0].id;
263
+ }
264
+ }
265
+ const normalized = normalizeModelId(modelId);
266
+ const normMatch = models.find((m) => normalizeModelId(m.id) === normalized);
267
+ if (normMatch) return normMatch.id;
268
+ consola.warn(`Model "${modelId}" not found in Copilot model list. Available: ${models.map((m) => m.id).join(", ")}`);
237
269
  return modelId;
238
270
  }
271
+ /**
272
+ * Resolve a codex model ID, falling back to the best available codex model.
273
+ * Used by the codex subcommand for model selection.
274
+ */
275
+ function resolveCodexModel(modelId) {
276
+ const resolved = resolveModel(modelId);
277
+ const models = state.models?.data;
278
+ if (!models) return resolved;
279
+ if (models.some((m) => m.id === resolved)) return resolved;
280
+ const codexModels = models.filter((m) => {
281
+ const endpoints = m.supported_endpoints ?? [];
282
+ return m.id.includes("codex") && !m.id.includes("mini") && (endpoints.length === 0 || endpoints.includes("/responses"));
283
+ });
284
+ if (codexModels.length > 0) {
285
+ codexModels.sort((a, b) => b.id.localeCompare(a.id));
286
+ const best = codexModels[0].id;
287
+ consola.warn(`Model "${modelId}" not available, using "${best}" instead`);
288
+ return best;
289
+ }
290
+ return resolved;
291
+ }
239
292
  async function cacheModels() {
240
293
  state.models = await getModels();
241
294
  }
@@ -413,10 +466,104 @@ const checkUsage = defineCommand({
413
466
  }
414
467
  });
415
468
 
469
+ //#endregion
470
+ //#region src/lib/file-log-reporter.ts
471
+ const MAX_LOG_BYTES = 1024 * 1024;
472
+ const DEDUP_MAX = 1e3;
473
+ const ARG_MAX_LEN = 2048;
474
+ const DEDUP_KEY_MAX_LEN = 200;
475
+ const CREDENTIAL_RE = /\b(eyJ[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]+){0,2}|gh[opsu]_[A-Za-z0-9_]{20,}|Bearer\s+\S{20,})\b/g;
476
+ const ALLOWED_TYPES = new Set([
477
+ "fatal",
478
+ "error",
479
+ "warn"
480
+ ]);
481
+ function sanitize(line) {
482
+ return line.replace(CREDENTIAL_RE, "[REDACTED]");
483
+ }
484
+ function serializeArg(arg) {
485
+ if (typeof arg === "string") return arg;
486
+ if (arg instanceof Error) {
487
+ const parts = [arg.message];
488
+ if (arg.stack) parts.push(arg.stack);
489
+ return parts.join("\n");
490
+ }
491
+ return String(arg);
492
+ }
493
+ function formatLogLine(logObj) {
494
+ return sanitize(`${logObj.date.toISOString()} [${(logObj.type ?? "error").toUpperCase()}] ${logObj.args.map((a) => {
495
+ const s = serializeArg(a);
496
+ return s.length > ARG_MAX_LEN ? s.slice(0, ARG_MAX_LEN) + "…" : s;
497
+ }).join(" ").replace(/\r\n|\r|\n/g, "\\n")}\n`);
498
+ }
499
+ function makeDedupeKey(logObj) {
500
+ const firstArg = logObj.args.length > 0 ? serializeArg(logObj.args[0]) : "";
501
+ const key = `${logObj.type}:${firstArg}`;
502
+ return key.length > DEDUP_KEY_MAX_LEN ? key.slice(0, DEDUP_KEY_MAX_LEN) : key;
503
+ }
504
+ function rotateIfNeeded(filePath) {
505
+ let size;
506
+ try {
507
+ size = fs$1.statSync(filePath).size;
508
+ } catch {
509
+ return;
510
+ }
511
+ if (size <= MAX_LOG_BYTES) return;
512
+ try {
513
+ fs$1.renameSync(filePath, filePath + ".1");
514
+ } catch {}
515
+ }
516
+ var FileLogReporter = class {
517
+ filePath;
518
+ seen = /* @__PURE__ */ new Set();
519
+ writing = false;
520
+ constructor(filePath) {
521
+ this.filePath = filePath;
522
+ rotateIfNeeded(filePath);
523
+ }
524
+ log(logObj, _ctx) {
525
+ if (!ALLOWED_TYPES.has(logObj.type)) return;
526
+ if (this.writing) return;
527
+ const key = makeDedupeKey(logObj);
528
+ if (this.seen.has(key)) return;
529
+ if (this.seen.size >= DEDUP_MAX) this.seen.clear();
530
+ this.seen.add(key);
531
+ const line = formatLogLine(logObj);
532
+ this.writing = true;
533
+ try {
534
+ const fd = fs$1.openSync(this.filePath, "a", 384);
535
+ fs$1.writeSync(fd, line);
536
+ fs$1.closeSync(fd);
537
+ } catch {} finally {
538
+ this.writing = false;
539
+ }
540
+ }
541
+ };
542
+ const nullStream = new Writable({ write(_chunk, _encoding, cb) {
543
+ cb();
544
+ } });
545
+ /**
546
+ * Switch consola to file-only mode for TUI sessions.
547
+ * Removes the terminal reporter and installs a file reporter that
548
+ * persists errors and warnings to disk with dedup and credential scrubbing.
549
+ *
550
+ * Also sinks consola's stdout/stderr streams as belt-and-suspenders:
551
+ * even if a terminal reporter is re-added, it cannot write to the terminal.
552
+ * Crash handlers that call process.stderr.write() directly are unaffected.
553
+ * FileLogReporter uses fs.writeSync() directly and is also unaffected.
554
+ */
555
+ function enableFileLogging() {
556
+ const reporter = new FileLogReporter(PATHS.ERROR_LOG_PATH);
557
+ consola.options.throttle = 0;
558
+ consola.setReporters([reporter]);
559
+ consola.options.stdout = nullStream;
560
+ consola.options.stderr = nullStream;
561
+ }
562
+
416
563
  //#endregion
417
564
  //#region src/lib/port.ts
418
565
  const DEFAULT_PORT = 8787;
419
- const DEFAULT_CODEX_MODEL = "gpt5.3-codex";
566
+ const DEFAULT_CODEX_MODEL = "gpt-5.3-codex";
420
567
  const PORT_RANGE_MIN = 11e3;
421
568
  const PORT_RANGE_MAX = 65535;
422
569
  /** Generate a random port number in the range [11000, 65535]. */
@@ -442,6 +589,7 @@ function buildLaunchCommand(target) {
442
589
  ...target.extraArgs
443
590
  ] : [
444
591
  "codex",
592
+ "--full-auto",
445
593
  "-m",
446
594
  target.model ?? DEFAULT_CODEX_MODEL,
447
595
  ...target.extraArgs
@@ -456,17 +604,26 @@ function launchChild(target, server$1) {
456
604
  const { cmd, env } = buildLaunchCommand(target);
457
605
  const executable = cmd[0];
458
606
  if (!commandExists(executable)) {
459
- consola.error(`"${executable}" not found on PATH. Install it first, then try again.`);
607
+ const msg = `"${executable}" not found on PATH. Install it first, then try again.`;
608
+ consola.error(msg);
609
+ process$1.stderr.write(msg + "\n");
460
610
  process$1.exit(1);
461
611
  }
462
612
  let child;
463
613
  try {
464
- child = spawn(cmd[0], cmd.slice(1), {
614
+ if (process$1.platform === "win32") child = spawn(cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" "), [], {
615
+ env,
616
+ stdio: "inherit",
617
+ shell: true
618
+ });
619
+ else child = spawn(cmd[0], cmd.slice(1), {
465
620
  env,
466
621
  stdio: "inherit"
467
622
  });
468
623
  } catch (error) {
469
- consola.error(`Failed to launch ${executable}:`, error instanceof Error ? error.message : String(error));
624
+ const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`;
625
+ consola.error(msg);
626
+ process$1.stderr.write(msg + "\n");
470
627
  server$1.close(true).catch(() => {});
471
628
  process$1.exit(1);
472
629
  }
@@ -494,10 +651,61 @@ function launchChild(target, server$1) {
494
651
  };
495
652
  process$1.on("SIGINT", onSignal);
496
653
  process$1.on("SIGTERM", onSignal);
497
- child.on("exit", (exitCode) => {
498
- cleanup().then(() => exit(exitCode ?? 0)).catch(() => exit(1));
654
+ child.on("exit", (exitCode, signal) => {
655
+ const code = exitCode ?? (signal ? 128 : 1);
656
+ cleanup().then(() => exit(code)).catch(() => exit(1));
657
+ });
658
+ child.on("error", () => {
659
+ cleanup().then(() => exit(1)).catch(() => exit(1));
499
660
  });
500
- child.on("error", () => exit(1));
661
+ }
662
+
663
+ //#endregion
664
+ //#region src/lib/model-validation.ts
665
+ const ENDPOINT_ALIASES = {
666
+ "/chat/completions": "/chat/completions",
667
+ "/v1/chat/completions": "/chat/completions",
668
+ "/responses": "/responses",
669
+ "/v1/responses": "/responses",
670
+ "/v1/messages": "/v1/messages"
671
+ };
672
+ /**
673
+ * Check whether a model supports the given endpoint, based on cached
674
+ * `supported_endpoints` metadata from the Copilot `/models` response.
675
+ *
676
+ * Returns `true` (allow) when:
677
+ * - the model is not found in the cache (don't block unknown models)
678
+ * - the model has no `supported_endpoints` field (backward-compat)
679
+ * - the endpoint is listed in `supported_endpoints`
680
+ */
681
+ function modelSupportsEndpoint(modelId, path$1) {
682
+ const endpoint = ENDPOINT_ALIASES[path$1] ?? path$1;
683
+ const model = state.models?.data.find((m) => m.id === modelId);
684
+ if (!model) return true;
685
+ const supported = model.supported_endpoints;
686
+ if (!supported || supported.length === 0) return true;
687
+ return supported.includes(endpoint);
688
+ }
689
+ /**
690
+ * Log an error when a model is used on an endpoint it doesn't support.
691
+ * Returns `true` if a mismatch was detected (for testing).
692
+ */
693
+ function logEndpointMismatch(modelId, path$1) {
694
+ if (modelSupportsEndpoint(modelId, path$1)) return false;
695
+ const supported = (state.models?.data.find((m) => m.id === modelId))?.supported_endpoints ?? [];
696
+ consola.error(`Model "${modelId}" does not support ${path$1}. Supported endpoints: ${supported.join(", ")}`);
697
+ return true;
698
+ }
699
+ /**
700
+ * Return model IDs that support the given endpoint.
701
+ */
702
+ function listModelsForEndpoint(path$1) {
703
+ const endpoint = ENDPOINT_ALIASES[path$1] ?? path$1;
704
+ return (state.models?.data ?? []).filter((m) => {
705
+ const supported = m.supported_endpoints;
706
+ if (!supported || supported.length === 0) return true;
707
+ return supported.includes(endpoint);
708
+ }).map((m) => m.id);
501
709
  }
502
710
 
503
711
  //#endregion
@@ -594,7 +802,7 @@ function formatTokens(n) {
594
802
  function formatTokenInfo(inputTokens, outputTokens, model) {
595
803
  if (inputTokens === void 0) return void 0;
596
804
  const parts = [];
597
- const maxPrompt = model?.capabilities.limits.max_prompt_tokens;
805
+ const maxPrompt = model?.capabilities?.limits?.max_prompt_tokens;
598
806
  if (maxPrompt) {
599
807
  const pct = (inputTokens / maxPrompt * 100).toFixed(1);
600
808
  parts.push(`in:${formatTokens(inputTokens)}/${formatTokens(maxPrompt)} (${pct}%)`);
@@ -714,7 +922,7 @@ const getEncodeChatFunction = async (encoding) => {
714
922
  * Get tokenizer type from model information
715
923
  */
716
924
  const getTokenizerFromModel = (model) => {
717
- return model.capabilities.tokenizer || "o200k_base";
925
+ return model.capabilities?.tokenizer || "o200k_base";
718
926
  };
719
927
  /**
720
928
  * Get model-specific constants for token calculation
@@ -948,6 +1156,7 @@ async function handleCompletion$1(c) {
948
1156
  const resolvedModel = resolveModel(payload.model);
949
1157
  if (resolvedModel !== payload.model) payload.model = resolvedModel;
950
1158
  const selectedModel = state.models?.data.find((model) => model.id === payload.model);
1159
+ logEndpointMismatch(payload.model, "/chat/completions");
951
1160
  let inputTokens;
952
1161
  try {
953
1162
  if (selectedModel) inputTokens = (await getTokenCount(payload, selectedModel)).input;
@@ -955,7 +1164,7 @@ async function handleCompletion$1(c) {
955
1164
  if (isNullish(payload.max_tokens)) {
956
1165
  payload = {
957
1166
  ...payload,
958
- max_tokens: selectedModel?.capabilities.limits.max_output_tokens
1167
+ max_tokens: selectedModel?.capabilities?.limits?.max_output_tokens
959
1168
  };
960
1169
  if (debugEnabled) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
961
1170
  }
@@ -1366,7 +1575,9 @@ async function handleCompletion(c) {
1366
1575
  if (state.manualApprove) await awaitApproval();
1367
1576
  const betaHeaders = extractBetaHeaders(c);
1368
1577
  const { body: resolvedBody, originalModel, resolvedModel } = resolveModelInBody(await processWebSearch(rawBody));
1369
- const selectedModel = state.models?.data.find((m) => m.id === (resolvedModel ?? originalModel));
1578
+ const modelId = resolvedModel ?? originalModel;
1579
+ const selectedModel = state.models?.data.find((m) => m.id === modelId);
1580
+ if (modelId) logEndpointMismatch(modelId, "/v1/messages");
1370
1581
  const effectiveBetas = applyDefaultBetas(betaHeaders, resolvedModel ?? originalModel);
1371
1582
  let response;
1372
1583
  try {
@@ -1591,12 +1802,9 @@ async function handleResponses(c) {
1591
1802
  const resolvedModel = resolveModel(payload.model);
1592
1803
  if (resolvedModel !== payload.model) payload.model = resolvedModel;
1593
1804
  const selectedModel = state.models?.data.find((model) => model.id === payload.model);
1805
+ logEndpointMismatch(payload.model, "/responses");
1594
1806
  if (state.manualApprove) await awaitApproval();
1595
1807
  await injectWebSearchIfNeeded(payload);
1596
- if (isNullish(payload.max_output_tokens)) {
1597
- payload.max_output_tokens = selectedModel?.capabilities.limits.max_output_tokens;
1598
- if (debugEnabled) consola.debug("Set max_output_tokens to:", JSON.stringify(payload.max_output_tokens));
1599
- }
1600
1808
  const response = await createResponses(payload, selectedModel?.requestHeaders).catch(async (error) => {
1601
1809
  if (error instanceof HTTPError) {
1602
1810
  const errorBody = await error.response.clone().text().catch(() => "");
@@ -1673,31 +1881,99 @@ function extractUserQuery(input) {
1673
1881
  }
1674
1882
  }
1675
1883
  }
1884
+ /**
1885
+ * Compaction prompt used when GitHub Copilot API does not support
1886
+ * /responses/compact natively. Matches the prompt Codex CLI uses for
1887
+ * local (non-OpenAI) compaction.
1888
+ */
1889
+ const COMPACTION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
1890
+
1891
+ Include:
1892
+ - Current progress and key decisions made
1893
+ - Important context, constraints, or user preferences
1894
+ - What remains to be done (clear next steps)
1895
+ - Any critical data, examples, or references needed to continue
1896
+
1897
+ Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
1676
1898
  async function handleResponsesCompact(c) {
1677
1899
  const startTime = Date.now();
1678
1900
  await checkRateLimit(state);
1679
1901
  if (!state.copilotToken) throw new Error("Copilot token not found");
1680
1902
  if (state.manualApprove) await awaitApproval();
1681
- const body = await c.req.text();
1903
+ const body = await c.req.json();
1682
1904
  const response = await fetch(`${copilotBaseUrl(state)}/responses/compact`, {
1683
1905
  method: "POST",
1684
1906
  headers: copilotHeaders(state),
1685
- body
1907
+ body: JSON.stringify(body)
1686
1908
  });
1687
- if (!response.ok) {
1909
+ if (response.ok) {
1688
1910
  logRequest({
1689
1911
  method: "POST",
1690
1912
  path: c.req.path,
1691
- status: response.status
1913
+ status: 200
1692
1914
  }, void 0, startTime);
1693
- throw new HTTPError("Copilot responses/compact request failed", response);
1915
+ return c.json(await response.json());
1916
+ }
1917
+ if (response.status === 404) {
1918
+ consola.debug("Copilot API does not support /responses/compact, using synthetic compaction");
1919
+ return await syntheticCompact(c, body, startTime);
1920
+ }
1921
+ logRequest({
1922
+ method: "POST",
1923
+ path: c.req.path,
1924
+ status: response.status
1925
+ }, void 0, startTime);
1926
+ throw new HTTPError("Copilot responses/compact request failed", response);
1927
+ }
1928
+ /**
1929
+ * Synthetic compaction: sends the conversation history to Copilot's
1930
+ * regular /responses endpoint with a compaction prompt appended,
1931
+ * then returns the model's summary in the compact response format.
1932
+ */
1933
+ async function syntheticCompact(c, body, startTime) {
1934
+ const input = Array.isArray(body.input) ? [...body.input] : [];
1935
+ input.push({
1936
+ type: "message",
1937
+ role: "user",
1938
+ content: [{
1939
+ type: "input_text",
1940
+ text: COMPACTION_PROMPT
1941
+ }]
1942
+ });
1943
+ const payload = {
1944
+ model: body.model,
1945
+ input,
1946
+ instructions: body.instructions,
1947
+ stream: false,
1948
+ store: false
1949
+ };
1950
+ let result;
1951
+ try {
1952
+ result = await createResponses(payload);
1953
+ } catch (error) {
1954
+ if (error instanceof HTTPError) logRequest({
1955
+ method: "POST",
1956
+ path: c.req.path,
1957
+ status: error.response.status
1958
+ }, void 0, startTime);
1959
+ throw error;
1694
1960
  }
1695
1961
  logRequest({
1696
1962
  method: "POST",
1697
1963
  path: c.req.path,
1698
1964
  status: 200
1699
1965
  }, void 0, startTime);
1700
- return c.json(await response.json());
1966
+ return c.json({
1967
+ id: `resp_compact_${randomUUID().replace(/-/g, "").slice(0, 24)}`,
1968
+ object: "response.compaction",
1969
+ created_at: Math.floor(Date.now() / 1e3),
1970
+ output: result.output,
1971
+ usage: result.usage ?? {
1972
+ input_tokens: 0,
1973
+ output_tokens: 0,
1974
+ total_tokens: 0
1975
+ }
1976
+ });
1701
1977
  }
1702
1978
 
1703
1979
  //#endregion
@@ -1799,7 +2075,7 @@ async function setupAndServe(options) {
1799
2075
  } else await setupGitHubToken();
1800
2076
  await setupCopilotToken();
1801
2077
  await cacheModels();
1802
- consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
2078
+ consola.debug(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
1803
2079
  const serveOptions = {
1804
2080
  fetch: server.fetch,
1805
2081
  hostname: "127.0.0.1",
@@ -1978,13 +2254,22 @@ const claude = defineCommand({
1978
2254
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
1979
2255
  process$1.exit(1);
1980
2256
  }
1981
- consola.success(`Server ready on ${serverUrl}, launching Claude Code...`);
1982
- consola.level = 1;
2257
+ enableFileLogging();
2258
+ let resolvedModel;
2259
+ if (args.model) {
2260
+ resolvedModel = resolveModel(args.model);
2261
+ if (resolvedModel !== args.model) consola.info(`Model "${args.model}" resolved to "${resolvedModel}"`);
2262
+ if (!state.models?.data.find((m) => m.id === resolvedModel)) {
2263
+ const available = listModelsForEndpoint("/v1/messages");
2264
+ consola.warn(`Model "${resolvedModel}" not found. Available claude models: ${available.join(", ")}`);
2265
+ }
2266
+ }
2267
+ process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code...\n`);
1983
2268
  launchChild({
1984
2269
  kind: "claude-code",
1985
- envVars: getClaudeCodeEnvVars(serverUrl, args.model),
2270
+ envVars: getClaudeCodeEnvVars(serverUrl, resolvedModel ?? args.model),
1986
2271
  extraArgs: args._ ?? [],
1987
- model: args.model
2272
+ model: resolvedModel ?? args.model
1988
2273
  }, server$1);
1989
2274
  }
1990
2275
  });
@@ -2024,14 +2309,24 @@ const codex = defineCommand({
2024
2309
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
2025
2310
  process$1.exit(1);
2026
2311
  }
2027
- const codexModel = args.model ?? DEFAULT_CODEX_MODEL;
2028
- consola.success(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...`);
2029
- consola.level = 1;
2312
+ const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
2313
+ enableFileLogging();
2314
+ const codexModel = resolveCodexModel(requestedModel);
2315
+ if (codexModel !== requestedModel) consola.info(`Model "${requestedModel}" resolved to "${codexModel}"`);
2316
+ const modelEntry = state.models?.data.find((m) => m.id === codexModel);
2317
+ if (!modelEntry) {
2318
+ const available = listModelsForEndpoint("/responses");
2319
+ consola.warn(`Model "${codexModel}" not found. Available codex models: ${available.join(", ")}`);
2320
+ } else {
2321
+ const ctx = modelEntry.capabilities?.limits?.max_context_window_tokens;
2322
+ if (ctx) consola.info(`Model context window: ${ctx.toLocaleString()} tokens`);
2323
+ }
2324
+ process$1.stderr.write(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...\n`);
2030
2325
  launchChild({
2031
2326
  kind: "codex",
2032
2327
  envVars: getCodexEnvVars(serverUrl),
2033
2328
  extraArgs: args._ ?? [],
2034
- model: args.model
2329
+ model: codexModel
2035
2330
  }, server$1);
2036
2331
  }
2037
2332
  });