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/LICENSE +21 -21
- package/README.md +206 -206
- package/dist/main.js +139 -17
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
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
|
-
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
2156
|
+
model: codexModel
|
|
2035
2157
|
}, server$1);
|
|
2036
2158
|
}
|
|
2037
2159
|
});
|