github-router 0.3.11 → 0.3.12

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
@@ -223,19 +223,68 @@ function filterBetaHeader(value) {
223
223
  return value.split(",").map((v) => v.trim()).filter((v) => v && ALLOWED_BETA_PREFIXES.some((prefix) => v.startsWith(prefix))).join(",") || void 0;
224
224
  }
225
225
  /**
226
+ * Normalize a model ID for fuzzy comparison: lowercase, replace dots with
227
+ * dashes, insert dash at letter→digit boundaries, and collapse repeated
228
+ * dashes. E.g. "gpt5.3-codex" → "gpt-5-3-codex", "GPT-5.3-Codex" → "gpt-5-3-codex".
229
+ */
230
+ function normalizeModelId(id) {
231
+ return id.toLowerCase().replace(/\./g, "-").replace(/([a-z])(\d)/g, "$1-$2").replace(/-{2,}/g, "-");
232
+ }
233
+ /**
226
234
  * Resolve a model name to the best available variant in the Copilot model list.
227
- * Prefers the 1M context variant for opus models.
235
+ *
236
+ * Resolution cascade:
237
+ * 1. Exact match
238
+ * 2. Case-insensitive match
239
+ * 3. Family preference (opus→1m, codex→highest version)
240
+ * 4. Normalized match (dots→dashes, letter-digit boundaries)
241
+ * 5. Return as-is with a warning
228
242
  */
229
243
  function resolveModel(modelId) {
230
244
  const models = state.models?.data;
231
245
  if (!models) return modelId;
232
246
  if (models.some((m) => m.id === modelId)) return modelId;
233
- if (modelId.toLowerCase().includes("opus")) {
247
+ const lower = modelId.toLowerCase();
248
+ const ciMatch = models.find((m) => m.id.toLowerCase() === lower);
249
+ if (ciMatch) return ciMatch.id;
250
+ if (lower.includes("opus")) {
234
251
  const oneM = models.find((m) => m.id.includes("opus") && m.id.endsWith("-1m"));
235
252
  if (oneM) return oneM.id;
236
253
  }
254
+ if (lower.includes("codex")) {
255
+ const codexModels = models.filter((m) => m.id.includes("codex") && !m.id.includes("mini"));
256
+ if (codexModels.length > 0) {
257
+ codexModels.sort((a, b) => b.id.localeCompare(a.id));
258
+ return codexModels[0].id;
259
+ }
260
+ }
261
+ const normalized = normalizeModelId(modelId);
262
+ const normMatch = models.find((m) => normalizeModelId(m.id) === normalized);
263
+ if (normMatch) return normMatch.id;
264
+ consola.warn(`Model "${modelId}" not found in Copilot model list. Available: ${models.map((m) => m.id).join(", ")}`);
237
265
  return modelId;
238
266
  }
267
+ /**
268
+ * Resolve a codex model ID, falling back to the best available codex model.
269
+ * Used by the codex subcommand for model selection.
270
+ */
271
+ function resolveCodexModel(modelId) {
272
+ const resolved = resolveModel(modelId);
273
+ const models = state.models?.data;
274
+ if (!models) return resolved;
275
+ if (models.some((m) => m.id === resolved)) return resolved;
276
+ const codexModels = models.filter((m) => {
277
+ const endpoints = m.supported_endpoints ?? [];
278
+ return m.id.includes("codex") && !m.id.includes("mini") && (endpoints.length === 0 || endpoints.includes("/responses"));
279
+ });
280
+ if (codexModels.length > 0) {
281
+ codexModels.sort((a, b) => b.id.localeCompare(a.id));
282
+ const best = codexModels[0].id;
283
+ consola.warn(`Model "${modelId}" not available, using "${best}" instead`);
284
+ return best;
285
+ }
286
+ return resolved;
287
+ }
239
288
  async function cacheModels() {
240
289
  state.models = await getModels();
241
290
  }
@@ -416,7 +465,7 @@ const checkUsage = defineCommand({
416
465
  //#endregion
417
466
  //#region src/lib/port.ts
418
467
  const DEFAULT_PORT = 8787;
419
- const DEFAULT_CODEX_MODEL = "gpt5.3-codex";
468
+ const DEFAULT_CODEX_MODEL = "gpt-5.3-codex";
420
469
  const PORT_RANGE_MIN = 11e3;
421
470
  const PORT_RANGE_MAX = 65535;
422
471
  /** Generate a random port number in the range [11000, 65535]. */
@@ -442,6 +491,7 @@ function buildLaunchCommand(target) {
442
491
  ...target.extraArgs
443
492
  ] : [
444
493
  "codex",
494
+ "--full-auto",
445
495
  "-m",
446
496
  target.model ?? DEFAULT_CODEX_MODEL,
447
497
  ...target.extraArgs
@@ -461,7 +511,12 @@ function launchChild(target, server$1) {
461
511
  }
462
512
  let child;
463
513
  try {
464
- child = spawn(cmd[0], cmd.slice(1), {
514
+ if (process$1.platform === "win32") child = spawn(cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" "), [], {
515
+ env,
516
+ stdio: "inherit",
517
+ shell: true
518
+ });
519
+ else child = spawn(cmd[0], cmd.slice(1), {
465
520
  env,
466
521
  stdio: "inherit"
467
522
  });
@@ -500,6 +555,54 @@ function launchChild(target, server$1) {
500
555
  child.on("error", () => exit(1));
501
556
  }
502
557
 
558
+ //#endregion
559
+ //#region src/lib/model-validation.ts
560
+ const ENDPOINT_ALIASES = {
561
+ "/chat/completions": "/chat/completions",
562
+ "/v1/chat/completions": "/chat/completions",
563
+ "/responses": "/responses",
564
+ "/v1/responses": "/responses",
565
+ "/v1/messages": "/v1/messages"
566
+ };
567
+ /**
568
+ * Check whether a model supports the given endpoint, based on cached
569
+ * `supported_endpoints` metadata from the Copilot `/models` response.
570
+ *
571
+ * Returns `true` (allow) when:
572
+ * - the model is not found in the cache (don't block unknown models)
573
+ * - the model has no `supported_endpoints` field (backward-compat)
574
+ * - the endpoint is listed in `supported_endpoints`
575
+ */
576
+ function modelSupportsEndpoint(modelId, path$1) {
577
+ const endpoint = ENDPOINT_ALIASES[path$1] ?? path$1;
578
+ const model = state.models?.data.find((m) => m.id === modelId);
579
+ if (!model) return true;
580
+ const supported = model.supported_endpoints;
581
+ if (!supported || supported.length === 0) return true;
582
+ return supported.includes(endpoint);
583
+ }
584
+ /**
585
+ * Log an error when a model is used on an endpoint it doesn't support.
586
+ * Returns `true` if a mismatch was detected (for testing).
587
+ */
588
+ function logEndpointMismatch(modelId, path$1) {
589
+ if (modelSupportsEndpoint(modelId, path$1)) return false;
590
+ const supported = (state.models?.data.find((m) => m.id === modelId))?.supported_endpoints ?? [];
591
+ consola.error(`Model "${modelId}" does not support ${path$1}. Supported endpoints: ${supported.join(", ")}`);
592
+ return true;
593
+ }
594
+ /**
595
+ * Return model IDs that support the given endpoint.
596
+ */
597
+ function listModelsForEndpoint(path$1) {
598
+ const endpoint = ENDPOINT_ALIASES[path$1] ?? path$1;
599
+ return (state.models?.data ?? []).filter((m) => {
600
+ const supported = m.supported_endpoints;
601
+ if (!supported || supported.length === 0) return true;
602
+ return supported.includes(endpoint);
603
+ }).map((m) => m.id);
604
+ }
605
+
503
606
  //#endregion
504
607
  //#region src/lib/proxy.ts
505
608
  function initProxyFromEnv() {
@@ -594,7 +697,7 @@ function formatTokens(n) {
594
697
  function formatTokenInfo(inputTokens, outputTokens, model) {
595
698
  if (inputTokens === void 0) return void 0;
596
699
  const parts = [];
597
- const maxPrompt = model?.capabilities.limits.max_prompt_tokens;
700
+ const maxPrompt = model?.capabilities?.limits?.max_prompt_tokens;
598
701
  if (maxPrompt) {
599
702
  const pct = (inputTokens / maxPrompt * 100).toFixed(1);
600
703
  parts.push(`in:${formatTokens(inputTokens)}/${formatTokens(maxPrompt)} (${pct}%)`);
@@ -714,7 +817,7 @@ const getEncodeChatFunction = async (encoding) => {
714
817
  * Get tokenizer type from model information
715
818
  */
716
819
  const getTokenizerFromModel = (model) => {
717
- return model.capabilities.tokenizer || "o200k_base";
820
+ return model.capabilities?.tokenizer || "o200k_base";
718
821
  };
719
822
  /**
720
823
  * Get model-specific constants for token calculation
@@ -948,6 +1051,7 @@ async function handleCompletion$1(c) {
948
1051
  const resolvedModel = resolveModel(payload.model);
949
1052
  if (resolvedModel !== payload.model) payload.model = resolvedModel;
950
1053
  const selectedModel = state.models?.data.find((model) => model.id === payload.model);
1054
+ logEndpointMismatch(payload.model, "/chat/completions");
951
1055
  let inputTokens;
952
1056
  try {
953
1057
  if (selectedModel) inputTokens = (await getTokenCount(payload, selectedModel)).input;
@@ -955,7 +1059,7 @@ async function handleCompletion$1(c) {
955
1059
  if (isNullish(payload.max_tokens)) {
956
1060
  payload = {
957
1061
  ...payload,
958
- max_tokens: selectedModel?.capabilities.limits.max_output_tokens
1062
+ max_tokens: selectedModel?.capabilities?.limits?.max_output_tokens
959
1063
  };
960
1064
  if (debugEnabled) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
961
1065
  }
@@ -1366,7 +1470,9 @@ async function handleCompletion(c) {
1366
1470
  if (state.manualApprove) await awaitApproval();
1367
1471
  const betaHeaders = extractBetaHeaders(c);
1368
1472
  const { body: resolvedBody, originalModel, resolvedModel } = resolveModelInBody(await processWebSearch(rawBody));
1369
- const selectedModel = state.models?.data.find((m) => m.id === (resolvedModel ?? originalModel));
1473
+ const modelId = resolvedModel ?? originalModel;
1474
+ const selectedModel = state.models?.data.find((m) => m.id === modelId);
1475
+ if (modelId) logEndpointMismatch(modelId, "/v1/messages");
1370
1476
  const effectiveBetas = applyDefaultBetas(betaHeaders, resolvedModel ?? originalModel);
1371
1477
  let response;
1372
1478
  try {
@@ -1591,12 +1697,9 @@ async function handleResponses(c) {
1591
1697
  const resolvedModel = resolveModel(payload.model);
1592
1698
  if (resolvedModel !== payload.model) payload.model = resolvedModel;
1593
1699
  const selectedModel = state.models?.data.find((model) => model.id === payload.model);
1700
+ logEndpointMismatch(payload.model, "/responses");
1594
1701
  if (state.manualApprove) await awaitApproval();
1595
1702
  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
1703
  const response = await createResponses(payload, selectedModel?.requestHeaders).catch(async (error) => {
1601
1704
  if (error instanceof HTTPError) {
1602
1705
  const errorBody = await error.response.clone().text().catch(() => "");
@@ -1799,7 +1902,7 @@ async function setupAndServe(options) {
1799
1902
  } else await setupGitHubToken();
1800
1903
  await setupCopilotToken();
1801
1904
  await cacheModels();
1802
- consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
1905
+ consola.debug(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
1803
1906
  const serveOptions = {
1804
1907
  fetch: server.fetch,
1805
1908
  hostname: "127.0.0.1",
@@ -1978,13 +2081,22 @@ const claude = defineCommand({
1978
2081
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
1979
2082
  process$1.exit(1);
1980
2083
  }
2084
+ let resolvedModel;
2085
+ if (args.model) {
2086
+ resolvedModel = resolveModel(args.model);
2087
+ if (resolvedModel !== args.model) consola.info(`Model "${args.model}" resolved to "${resolvedModel}"`);
2088
+ if (!state.models?.data.find((m) => m.id === resolvedModel)) {
2089
+ const available = listModelsForEndpoint("/v1/messages");
2090
+ consola.warn(`Model "${resolvedModel}" not found. Available claude models: ${available.join(", ")}`);
2091
+ }
2092
+ }
1981
2093
  consola.success(`Server ready on ${serverUrl}, launching Claude Code...`);
1982
2094
  consola.level = 1;
1983
2095
  launchChild({
1984
2096
  kind: "claude-code",
1985
- envVars: getClaudeCodeEnvVars(serverUrl, args.model),
2097
+ envVars: getClaudeCodeEnvVars(serverUrl, resolvedModel ?? args.model),
1986
2098
  extraArgs: args._ ?? [],
1987
- model: args.model
2099
+ model: resolvedModel ?? args.model
1988
2100
  }, server$1);
1989
2101
  }
1990
2102
  });
@@ -2024,14 +2136,24 @@ const codex = defineCommand({
2024
2136
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
2025
2137
  process$1.exit(1);
2026
2138
  }
2027
- const codexModel = args.model ?? DEFAULT_CODEX_MODEL;
2139
+ const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
2140
+ const codexModel = resolveCodexModel(requestedModel);
2141
+ if (codexModel !== requestedModel) consola.info(`Model "${requestedModel}" resolved to "${codexModel}"`);
2142
+ const modelEntry = state.models?.data.find((m) => m.id === codexModel);
2143
+ if (!modelEntry) {
2144
+ const available = listModelsForEndpoint("/responses");
2145
+ consola.warn(`Model "${codexModel}" not found. Available codex models: ${available.join(", ")}`);
2146
+ } else {
2147
+ const ctx = modelEntry.capabilities?.limits?.max_context_window_tokens;
2148
+ if (ctx) consola.info(`Model context window: ${ctx.toLocaleString()} tokens`);
2149
+ }
2028
2150
  consola.success(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...`);
2029
2151
  consola.level = 1;
2030
2152
  launchChild({
2031
2153
  kind: "codex",
2032
2154
  envVars: getCodexEnvVars(serverUrl),
2033
2155
  extraArgs: args._ ?? [],
2034
- model: args.model
2156
+ model: codexModel
2035
2157
  }, server$1);
2036
2158
  }
2037
2159
  });