llm-cli-gateway 1.0.1 → 1.4.0
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/CHANGELOG.md +42 -0
- package/README.md +153 -9
- package/dist/approval-manager.d.ts +1 -1
- package/dist/approval-manager.js +7 -4
- package/dist/async-job-manager.d.ts +53 -4
- package/dist/async-job-manager.js +254 -27
- package/dist/claude-mcp-config.js +7 -4
- package/dist/cli-updater.d.ts +38 -0
- package/dist/cli-updater.js +145 -0
- package/dist/config.js +15 -9
- package/dist/db.js +4 -4
- package/dist/executor.js +20 -13
- package/dist/flight-recorder.d.ts +48 -0
- package/dist/flight-recorder.js +220 -0
- package/dist/health.js +3 -3
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1456 -278
- package/dist/job-store.d.ts +84 -0
- package/dist/job-store.js +251 -0
- package/dist/logger.js +1 -1
- package/dist/metrics.js +9 -12
- package/dist/migrate-sessions.js +2 -2
- package/dist/model-registry.d.ts +14 -0
- package/dist/model-registry.js +448 -140
- package/dist/optimizer.js +9 -9
- package/dist/process-monitor.js +24 -8
- package/dist/request-helpers.d.ts +48 -0
- package/dist/request-helpers.js +64 -2
- package/dist/resources.js +76 -32
- package/dist/retry.js +6 -4
- package/dist/review-integrity.d.ts +6 -38
- package/dist/review-integrity.js +41 -275
- package/dist/session-manager-pg.js +7 -4
- package/dist/session-manager.d.ts +1 -1
- package/dist/session-manager.js +9 -5
- package/dist/stream-json-parser.js +8 -6
- package/package.json +7 -4
package/dist/model-registry.js
CHANGED
|
@@ -1,37 +1,57 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { parse as parseToml } from "toml";
|
|
4
5
|
const FALLBACK_INFO = {
|
|
5
6
|
claude: {
|
|
6
7
|
description: "Anthropic's Claude Code CLI - best for code generation, analysis, and agentic coding tasks",
|
|
7
8
|
models: {
|
|
8
9
|
opus: "Most capable model. Best for: complex reasoning, nuanced analysis, difficult problems, research",
|
|
9
10
|
sonnet: "Balanced performance. Best for: everyday coding, code review, general tasks (default)",
|
|
10
|
-
haiku: "Fastest model. Best for: simple queries, quick answers, high-volume tasks, cost-sensitive use"
|
|
11
|
+
haiku: "Fastest model. Best for: simple queries, quick answers, high-volume tasks, cost-sensitive use",
|
|
11
12
|
},
|
|
12
13
|
defaultModel: "sonnet",
|
|
13
|
-
modelOrder: ["opus", "sonnet", "haiku"]
|
|
14
|
+
modelOrder: ["opus", "sonnet", "haiku"],
|
|
14
15
|
},
|
|
15
16
|
codex: {
|
|
17
|
+
// Note: no hardcoded `defaultModel`. Let Codex CLI pick its own built-in default
|
|
18
|
+
// unless an explicit value is found via config.toml / env vars in applyCodexOverrides.
|
|
19
|
+
// This prevents the gateway from pinning a model that may become deprecated upstream.
|
|
16
20
|
description: "OpenAI's Codex CLI - best for code execution in sandboxed environments",
|
|
17
21
|
models: {
|
|
18
22
|
"gpt-5.4": "Frontier coding and professional-work model. Best for: most Codex tasks, long-running agentic work",
|
|
19
23
|
"gpt-5.3-codex": "Specialized Codex model. Best for: agentic coding workflows with Codex-tuned behavior",
|
|
20
24
|
"gpt-5.2": "Strong general-purpose GPT-5 model. Best for: broad coding and reasoning tasks",
|
|
21
|
-
"gpt-5-pro": "Highest-capability GPT-5 model. Best for: deep reasoning and difficult professional workflows"
|
|
25
|
+
"gpt-5-pro": "Highest-capability GPT-5 model. Best for: deep reasoning and difficult professional workflows",
|
|
22
26
|
},
|
|
23
|
-
|
|
24
|
-
modelOrder: ["gpt-5.4", "gpt-5.3-codex", "gpt-5.2", "gpt-5-pro"]
|
|
27
|
+
modelOrder: ["gpt-5.3-codex", "gpt-5.4", "gpt-5.2", "gpt-5-pro"],
|
|
25
28
|
},
|
|
26
29
|
gemini: {
|
|
27
30
|
description: "Google's Gemini CLI - best for multimodal tasks and Google ecosystem integration",
|
|
28
31
|
models: {
|
|
29
32
|
"gemini-2.5-pro": "Most capable model. Best for: complex reasoning, long context, multimodal",
|
|
30
|
-
"gemini-2.5-flash": "Fast model. Best for: quick responses, high throughput, cost-sensitive use"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
+
"gemini-2.5-flash": "Fast model. Best for: quick responses, high throughput, cost-sensitive use",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
grok: {
|
|
37
|
+
// No hardcoded `defaultModel`. Let Grok CLI pick its own built-in default
|
|
38
|
+
// unless an explicit value is found via env vars in applyGrokOverrides.
|
|
39
|
+
description: "xAI's Grok Build CLI - best for agentic coding tasks via xAI's Grok models",
|
|
40
|
+
models: {
|
|
41
|
+
"grok-build": "Default Grok model for code/agentic tasks. Best for: most Grok build sessions",
|
|
42
|
+
},
|
|
43
|
+
modelOrder: ["grok-build"],
|
|
44
|
+
},
|
|
33
45
|
};
|
|
34
46
|
const MODEL_CACHE_TTL_MS = 2 * 60 * 1000;
|
|
47
|
+
const MAX_GEMINI_HISTORY_FILES = 200;
|
|
48
|
+
const MAX_GEMINI_HISTORY_FILE_BYTES = 2 * 1024 * 1024;
|
|
49
|
+
const SOURCE_PRIORITY = {
|
|
50
|
+
fallback: 0,
|
|
51
|
+
observed: 1,
|
|
52
|
+
config: 2,
|
|
53
|
+
env: 3,
|
|
54
|
+
};
|
|
35
55
|
let cachedInfo = null;
|
|
36
56
|
export function getCliInfo(forceRefresh = false) {
|
|
37
57
|
if (!forceRefresh && cachedInfo && Date.now() - cachedInfo.loadedAt < MODEL_CACHE_TTL_MS) {
|
|
@@ -41,6 +61,9 @@ export function getCliInfo(forceRefresh = false) {
|
|
|
41
61
|
cachedInfo = { loadedAt: Date.now(), info };
|
|
42
62
|
return info;
|
|
43
63
|
}
|
|
64
|
+
export function clearModelRegistryCache() {
|
|
65
|
+
cachedInfo = null;
|
|
66
|
+
}
|
|
44
67
|
export function resolveModelAlias(cli, model, info) {
|
|
45
68
|
if (!model) {
|
|
46
69
|
return undefined;
|
|
@@ -52,7 +75,14 @@ export function resolveModelAlias(cli, model, info) {
|
|
|
52
75
|
const normalized = trimmed.toLowerCase();
|
|
53
76
|
const cliInfo = info[cli];
|
|
54
77
|
if (normalized === "default" || normalized === "latest") {
|
|
55
|
-
return
|
|
78
|
+
// If no default is configured, return undefined so the CLI picks its own
|
|
79
|
+
// built-in default. Avoids passing the literal string "default"/"latest"
|
|
80
|
+
// as a model name to the CLI.
|
|
81
|
+
return cliInfo.defaultModel;
|
|
82
|
+
}
|
|
83
|
+
const alias = resolveConfiguredAlias(cliInfo, normalized);
|
|
84
|
+
if (alias !== undefined) {
|
|
85
|
+
return alias;
|
|
56
86
|
}
|
|
57
87
|
if (cli === "gemini") {
|
|
58
88
|
if (normalized === "flash" || normalized === "pro") {
|
|
@@ -66,164 +96,404 @@ function buildCliInfo() {
|
|
|
66
96
|
const info = {
|
|
67
97
|
claude: cloneInfo(FALLBACK_INFO.claude),
|
|
68
98
|
codex: cloneInfo(FALLBACK_INFO.codex),
|
|
69
|
-
gemini: cloneInfo(FALLBACK_INFO.gemini)
|
|
99
|
+
gemini: cloneInfo(FALLBACK_INFO.gemini),
|
|
100
|
+
grok: cloneInfo(FALLBACK_INFO.grok),
|
|
70
101
|
};
|
|
71
102
|
applyClaudeOverrides(info.claude);
|
|
72
103
|
applyCodexOverrides(info.codex);
|
|
73
104
|
applyGeminiOverrides(info.gemini);
|
|
105
|
+
applyGrokOverrides(info.grok);
|
|
74
106
|
return info;
|
|
75
107
|
}
|
|
76
108
|
function cloneInfo(source) {
|
|
77
|
-
|
|
109
|
+
const cloned = {
|
|
78
110
|
description: source.description,
|
|
79
111
|
models: { ...source.models },
|
|
80
112
|
defaultModel: source.defaultModel,
|
|
81
|
-
|
|
113
|
+
defaultModelSource: source.defaultModelSource,
|
|
114
|
+
modelOrder: source.modelOrder ? [...source.modelOrder] : undefined,
|
|
115
|
+
aliases: source.aliases ? { ...source.aliases } : undefined,
|
|
116
|
+
modelMetadata: source.modelMetadata ? { ...source.modelMetadata } : {},
|
|
117
|
+
warnings: source.warnings ? [...source.warnings] : [],
|
|
82
118
|
};
|
|
119
|
+
Object.keys(cloned.models).forEach(model => {
|
|
120
|
+
cloned.modelMetadata[model] = cloned.modelMetadata[model] ?? {
|
|
121
|
+
source: "fallback",
|
|
122
|
+
sourceDetail: "Bundled fallback registry",
|
|
123
|
+
confidence: "low",
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return cloned;
|
|
127
|
+
}
|
|
128
|
+
function addWarning(info, warning) {
|
|
129
|
+
info.warnings = info.warnings ?? [];
|
|
130
|
+
if (!info.warnings.includes(warning)) {
|
|
131
|
+
info.warnings.push(warning);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isValidModelName(model) {
|
|
135
|
+
return model.length > 0 && !/[\u0000-\u001f\u007f]/.test(model) && !/\s/.test(model);
|
|
136
|
+
}
|
|
137
|
+
function addModel(info, model, description, metadata, options = {}) {
|
|
138
|
+
const normalized = model.trim();
|
|
139
|
+
if (!isValidModelName(normalized)) {
|
|
140
|
+
addWarning(info, `Ignored invalid model name from ${metadata.sourceDetail}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const existingMetadata = info.modelMetadata?.[normalized];
|
|
144
|
+
const existingPriority = existingMetadata ? SOURCE_PRIORITY[existingMetadata.source] : -1;
|
|
145
|
+
const incomingPriority = SOURCE_PRIORITY[metadata.source];
|
|
146
|
+
if (!info.models[normalized] ||
|
|
147
|
+
options.preferDescription ||
|
|
148
|
+
incomingPriority > existingPriority) {
|
|
149
|
+
info.models[normalized] = description;
|
|
150
|
+
}
|
|
151
|
+
info.modelMetadata = info.modelMetadata ?? {};
|
|
152
|
+
if (!existingMetadata || incomingPriority >= existingPriority) {
|
|
153
|
+
info.modelMetadata[normalized] = metadata;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function setDefaultModel(info, model, source, sourceType) {
|
|
157
|
+
const normalized = model?.trim();
|
|
158
|
+
if (!normalized) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!isValidModelName(normalized)) {
|
|
162
|
+
addWarning(info, `Ignored invalid default model from ${source}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
addModel(info, normalized, `Configured default from ${source}`, {
|
|
166
|
+
source: sourceType,
|
|
167
|
+
sourceDetail: source,
|
|
168
|
+
confidence: sourceType === "env" ? "high" : "medium",
|
|
169
|
+
});
|
|
170
|
+
info.defaultModel = normalized;
|
|
171
|
+
info.defaultModelSource = source;
|
|
172
|
+
}
|
|
173
|
+
function addAlias(info, alias, target, source) {
|
|
174
|
+
const normalizedAlias = alias.trim().toLowerCase();
|
|
175
|
+
const normalizedTarget = target.trim();
|
|
176
|
+
if (!normalizedAlias || /\s/.test(normalizedAlias) || !normalizedTarget) {
|
|
177
|
+
addWarning(info, `Ignored invalid alias from ${source}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (normalizedTarget !== "default" && !isValidModelName(normalizedTarget)) {
|
|
181
|
+
addWarning(info, `Ignored invalid alias target from ${source}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
info.aliases = info.aliases ?? {};
|
|
185
|
+
info.aliases[normalizedAlias] = normalizedTarget;
|
|
186
|
+
}
|
|
187
|
+
function resolveConfiguredAlias(info, normalizedAlias) {
|
|
188
|
+
const target = info.aliases?.[normalizedAlias];
|
|
189
|
+
if (!target) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
if (target === "default") {
|
|
193
|
+
return info.defaultModel;
|
|
194
|
+
}
|
|
195
|
+
return target;
|
|
196
|
+
}
|
|
197
|
+
function addEnvModels(info, envName) {
|
|
198
|
+
const entries = parseEnvModelEntries(process.env[envName], envName);
|
|
199
|
+
entries.forEach(entry => {
|
|
200
|
+
addModel(info, entry.model, entry.description ?? `Configured via ${envName}`, {
|
|
201
|
+
source: "env",
|
|
202
|
+
sourceDetail: envName,
|
|
203
|
+
confidence: "high",
|
|
204
|
+
}, { preferDescription: Boolean(entry.description) });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function addEnvAliases(info, cli, envName) {
|
|
208
|
+
parseEnvAliasEntries(process.env[envName], envName, cli).forEach(entry => {
|
|
209
|
+
addAlias(info, entry.alias, entry.target, envName);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function addGlobalEnvAliases(info, cli) {
|
|
213
|
+
parseEnvAliasEntries(process.env.LLM_GATEWAY_MODEL_ALIASES, "LLM_GATEWAY_MODEL_ALIASES", cli).forEach(entry => {
|
|
214
|
+
addAlias(info, entry.alias, entry.target, "LLM_GATEWAY_MODEL_ALIASES");
|
|
215
|
+
});
|
|
83
216
|
}
|
|
84
217
|
function applyClaudeOverrides(info) {
|
|
85
|
-
const settingsPath = path.join(homedir(), ".claude", "settings.json");
|
|
86
|
-
const settingsLocalPath =
|
|
218
|
+
const settingsPath = process.env.CLAUDE_SETTINGS_PATH || path.join(homedir(), ".claude", "settings.json");
|
|
219
|
+
const settingsLocalPath = process.env.CLAUDE_SETTINGS_LOCAL_PATH ||
|
|
220
|
+
path.join(homedir(), ".claude", "settings.local.json");
|
|
87
221
|
const envDefault = process.env.CLAUDE_DEFAULT_MODEL;
|
|
88
|
-
const localDefault =
|
|
89
|
-
const settingsDefault =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
? "CLAUDE_DEFAULT_MODEL"
|
|
93
|
-
: localDefault
|
|
94
|
-
? settingsLocalPath
|
|
95
|
-
: settingsPath;
|
|
96
|
-
if (defaultModel && typeof defaultModel === "string") {
|
|
97
|
-
if (!info.models[defaultModel]) {
|
|
98
|
-
info.models[defaultModel] = `Configured default from ${defaultSource}`;
|
|
99
|
-
}
|
|
100
|
-
info.defaultModel = defaultModel;
|
|
222
|
+
const localDefault = readJsonStringValue(settingsLocalPath, [["model"], ["model", "name"]], info);
|
|
223
|
+
const settingsDefault = readJsonStringValue(settingsPath, [["model"], ["model", "name"]], info);
|
|
224
|
+
if (settingsDefault) {
|
|
225
|
+
setDefaultModel(info, settingsDefault, settingsPath, "config");
|
|
101
226
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
envModels.forEach(model => {
|
|
105
|
-
if (!info.models[model]) {
|
|
106
|
-
info.models[model] = "Configured via CLAUDE_MODELS";
|
|
107
|
-
}
|
|
108
|
-
});
|
|
227
|
+
if (localDefault) {
|
|
228
|
+
setDefaultModel(info, localDefault, settingsLocalPath, "config");
|
|
109
229
|
}
|
|
230
|
+
if (envDefault) {
|
|
231
|
+
setDefaultModel(info, envDefault, "CLAUDE_DEFAULT_MODEL", "env");
|
|
232
|
+
}
|
|
233
|
+
addEnvModels(info, "CLAUDE_MODELS");
|
|
234
|
+
addEnvAliases(info, "claude", "CLAUDE_MODEL_ALIASES");
|
|
235
|
+
addGlobalEnvAliases(info, "claude");
|
|
110
236
|
info.modelOrder = buildOrder(info, info.defaultModel);
|
|
111
237
|
}
|
|
112
238
|
function applyCodexOverrides(info) {
|
|
113
239
|
const configPath = process.env.CODEX_CONFIG_PATH || path.join(homedir(), ".codex", "config.toml");
|
|
114
240
|
const envDefault = process.env.CODEX_DEFAULT_MODEL;
|
|
115
|
-
const envModels = parseEnvModels(process.env.CODEX_MODELS);
|
|
116
|
-
const detectedModels = {};
|
|
117
|
-
let defaultModel;
|
|
118
241
|
if (existsSync(configPath)) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
242
|
+
try {
|
|
243
|
+
const content = readFileSync(configPath, "utf-8");
|
|
244
|
+
const parsed = parseToml(content);
|
|
245
|
+
const model = readStringProperty(parsed, "model");
|
|
246
|
+
setDefaultModel(info, model, configPath, "config");
|
|
247
|
+
const profiles = readRecordProperty(parsed, "profiles");
|
|
248
|
+
Object.entries(profiles).forEach(([profileName, profile]) => {
|
|
249
|
+
const profileModel = readStringProperty(profile, "model");
|
|
250
|
+
if (profileModel) {
|
|
251
|
+
addModel(info, profileModel, `Configured in Codex profile '${profileName}'`, {
|
|
252
|
+
source: "config",
|
|
253
|
+
sourceDetail: `${configPath} profile ${profileName}`,
|
|
254
|
+
confidence: "medium",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
const notice = readRecordProperty(parsed, "notice");
|
|
259
|
+
const migrations = readRecordProperty(notice, "model_migrations");
|
|
260
|
+
Object.entries(migrations).forEach(([from, to]) => {
|
|
261
|
+
if (typeof to !== "string") {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
addModel(info, to, `Migration target for ${from}`, {
|
|
265
|
+
source: "config",
|
|
266
|
+
sourceDetail: `${configPath} notice.model_migrations`,
|
|
267
|
+
confidence: "medium",
|
|
268
|
+
});
|
|
269
|
+
addModel(info, from, `Legacy model (migrates to ${to})`, {
|
|
270
|
+
source: "config",
|
|
271
|
+
sourceDetail: `${configPath} notice.model_migrations`,
|
|
272
|
+
confidence: "medium",
|
|
273
|
+
});
|
|
274
|
+
});
|
|
124
275
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
detectedModels[to] = `Migrated from ${from}`;
|
|
129
|
-
}
|
|
130
|
-
if (!detectedModels[from]) {
|
|
131
|
-
detectedModels[from] = `Legacy model (migrates to ${to})`;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
envModels.forEach(model => {
|
|
136
|
-
if (!detectedModels[model]) {
|
|
137
|
-
detectedModels[model] = "Configured via CODEX_MODELS";
|
|
276
|
+
catch (error) {
|
|
277
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
278
|
+
addWarning(info, `Could not parse Codex config ${configPath}: ${message}`);
|
|
138
279
|
}
|
|
139
|
-
});
|
|
140
|
-
if (envDefault) {
|
|
141
|
-
detectedModels[envDefault] = detectedModels[envDefault] || "Default from CODEX_DEFAULT_MODEL";
|
|
142
|
-
defaultModel = envDefault;
|
|
143
280
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
info.defaultModel = defaultModel;
|
|
281
|
+
addEnvModels(info, "CODEX_MODELS");
|
|
282
|
+
addEnvAliases(info, "codex", "CODEX_MODEL_ALIASES");
|
|
283
|
+
addGlobalEnvAliases(info, "codex");
|
|
284
|
+
if (envDefault) {
|
|
285
|
+
setDefaultModel(info, envDefault, "CODEX_DEFAULT_MODEL", "env");
|
|
150
286
|
}
|
|
151
287
|
info.modelOrder = buildOrder(info, info.defaultModel);
|
|
152
288
|
}
|
|
153
289
|
function applyGeminiOverrides(info) {
|
|
290
|
+
const settingsPath = process.env.GEMINI_SETTINGS_PATH || path.join(homedir(), ".gemini", "settings.json");
|
|
291
|
+
const settingsDefault = readJsonStringValue(settingsPath, [["model"], ["model", "name"], ["selectedModel"], ["defaultModel"]], info);
|
|
154
292
|
const envDefault = process.env.GEMINI_DEFAULT_MODEL;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
293
|
+
if (settingsDefault) {
|
|
294
|
+
setDefaultModel(info, settingsDefault, settingsPath, "config");
|
|
295
|
+
}
|
|
296
|
+
if (!isModelDiscoveryDisabled()) {
|
|
297
|
+
const observed = collectGeminiModels();
|
|
298
|
+
observed.forEach(observation => {
|
|
299
|
+
addModel(info, observation.model, `Observed in local Gemini sessions (last seen ${observation.lastSeen})`, {
|
|
300
|
+
source: "observed",
|
|
301
|
+
sourceDetail: observation.filePath,
|
|
302
|
+
confidence: "low",
|
|
303
|
+
lastSeen: observation.lastSeen,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
addEnvModels(info, "GEMINI_MODELS");
|
|
308
|
+
addEnvAliases(info, "gemini", "GEMINI_MODEL_ALIASES");
|
|
309
|
+
addGlobalEnvAliases(info, "gemini");
|
|
167
310
|
if (envDefault) {
|
|
168
|
-
info
|
|
169
|
-
info.defaultModel = envDefault;
|
|
311
|
+
setDefaultModel(info, envDefault, "GEMINI_DEFAULT_MODEL", "env");
|
|
170
312
|
}
|
|
171
313
|
info.modelOrder = buildOrder(info, info.defaultModel);
|
|
172
314
|
}
|
|
173
|
-
function
|
|
315
|
+
function applyGrokOverrides(info) {
|
|
316
|
+
const envDefault = process.env.GROK_DEFAULT_MODEL;
|
|
317
|
+
addEnvModels(info, "GROK_MODELS");
|
|
318
|
+
addEnvAliases(info, "grok", "GROK_MODEL_ALIASES");
|
|
319
|
+
addGlobalEnvAliases(info, "grok");
|
|
320
|
+
if (envDefault) {
|
|
321
|
+
setDefaultModel(info, envDefault, "GROK_DEFAULT_MODEL", "env");
|
|
322
|
+
}
|
|
323
|
+
info.modelOrder = buildOrder(info, info.defaultModel);
|
|
324
|
+
}
|
|
325
|
+
function readJsonStringValue(filePath, paths, info) {
|
|
174
326
|
if (!existsSync(filePath)) {
|
|
175
327
|
return undefined;
|
|
176
328
|
}
|
|
177
329
|
try {
|
|
178
330
|
const content = readFileSync(filePath, "utf-8");
|
|
179
331
|
const parsed = JSON.parse(content);
|
|
180
|
-
const
|
|
181
|
-
|
|
332
|
+
for (const pathParts of paths) {
|
|
333
|
+
const value = readPath(parsed, pathParts);
|
|
334
|
+
if (typeof value === "string" && value.trim()) {
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
182
339
|
}
|
|
183
|
-
catch {
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (info) {
|
|
342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
343
|
+
addWarning(info, `Could not parse JSON config ${filePath}: ${message}`);
|
|
344
|
+
}
|
|
184
345
|
return undefined;
|
|
185
346
|
}
|
|
186
347
|
}
|
|
187
|
-
function
|
|
348
|
+
function readPath(value, pathParts) {
|
|
349
|
+
let current = value;
|
|
350
|
+
for (const part of pathParts) {
|
|
351
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
current = current[part];
|
|
355
|
+
}
|
|
356
|
+
return current;
|
|
357
|
+
}
|
|
358
|
+
function readStringProperty(value, key) {
|
|
359
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
const prop = value[key];
|
|
363
|
+
return typeof prop === "string" && prop.trim() ? prop : undefined;
|
|
364
|
+
}
|
|
365
|
+
function readRecordProperty(value, key) {
|
|
366
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
367
|
+
return {};
|
|
368
|
+
}
|
|
369
|
+
const prop = value[key];
|
|
370
|
+
return prop && typeof prop === "object" && !Array.isArray(prop)
|
|
371
|
+
? prop
|
|
372
|
+
: {};
|
|
373
|
+
}
|
|
374
|
+
function parseEnvModelEntries(value, source) {
|
|
188
375
|
if (!value) {
|
|
189
376
|
return [];
|
|
190
377
|
}
|
|
191
|
-
|
|
378
|
+
const trimmed = value.trim();
|
|
379
|
+
if (!trimmed) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
383
|
+
try {
|
|
384
|
+
const parsed = JSON.parse(trimmed);
|
|
385
|
+
if (Array.isArray(parsed)) {
|
|
386
|
+
return parsed.flatMap(entry => {
|
|
387
|
+
if (typeof entry === "string") {
|
|
388
|
+
return [{ model: entry }];
|
|
389
|
+
}
|
|
390
|
+
if (entry && typeof entry === "object") {
|
|
391
|
+
const record = entry;
|
|
392
|
+
const model = firstString(record.model, record.id, record.name);
|
|
393
|
+
const description = firstString(record.description, record.desc);
|
|
394
|
+
return model ? [{ model, description }] : [];
|
|
395
|
+
}
|
|
396
|
+
return [];
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (parsed && typeof parsed === "object") {
|
|
400
|
+
return Object.entries(parsed).flatMap(([model, description]) => {
|
|
401
|
+
if (typeof description === "string") {
|
|
402
|
+
return [{ model, description }];
|
|
403
|
+
}
|
|
404
|
+
return [{ model }];
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return [{ model: trimmed, description: `Configured via ${source}` }];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return trimmed
|
|
192
413
|
.split(/[,\n]/)
|
|
193
414
|
.map(entry => entry.trim())
|
|
194
|
-
.filter(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
415
|
+
.filter(Boolean)
|
|
416
|
+
.map(entry => {
|
|
417
|
+
const eqIndex = entry.indexOf("=");
|
|
418
|
+
if (eqIndex > 0) {
|
|
419
|
+
return {
|
|
420
|
+
model: entry.slice(0, eqIndex).trim(),
|
|
421
|
+
description: entry.slice(eqIndex + 1).trim() || undefined,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return { model: entry };
|
|
425
|
+
});
|
|
200
426
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!tableMatch || tableMatch.index === undefined) {
|
|
205
|
-
return {};
|
|
427
|
+
function parseEnvAliasEntries(value, source, cli) {
|
|
428
|
+
if (!value) {
|
|
429
|
+
return [];
|
|
206
430
|
}
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
431
|
+
const trimmed = value.trim();
|
|
432
|
+
if (!trimmed) {
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
const entries = [];
|
|
436
|
+
const addEntry = (rawAlias, rawTarget) => {
|
|
437
|
+
if (typeof rawTarget !== "string") {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
let alias = rawAlias.trim();
|
|
441
|
+
if (source === "LLM_GATEWAY_MODEL_ALIASES") {
|
|
442
|
+
const prefix = `${cli}.`;
|
|
443
|
+
if (!alias.startsWith(prefix)) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
alias = alias.slice(prefix.length);
|
|
447
|
+
}
|
|
448
|
+
entries.push({ alias, target: rawTarget });
|
|
449
|
+
};
|
|
450
|
+
if (trimmed.startsWith("{")) {
|
|
451
|
+
try {
|
|
452
|
+
const parsed = JSON.parse(trimmed);
|
|
453
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
454
|
+
Object.entries(parsed).forEach(([alias, target]) => addEntry(alias, target));
|
|
455
|
+
return entries;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
trimmed.split(/[,\n]/).forEach(entry => {
|
|
463
|
+
const eqIndex = entry.indexOf("=");
|
|
464
|
+
if (eqIndex <= 0) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
addEntry(entry.slice(0, eqIndex), entry.slice(eqIndex + 1));
|
|
468
|
+
});
|
|
469
|
+
return entries;
|
|
470
|
+
}
|
|
471
|
+
function firstString(...values) {
|
|
472
|
+
for (const value of values) {
|
|
473
|
+
if (typeof value === "string" && value.trim()) {
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
216
476
|
}
|
|
217
|
-
return
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
function isModelDiscoveryDisabled() {
|
|
480
|
+
return (process.env.LLM_GATEWAY_DISABLE_MODEL_DISCOVERY === "1" ||
|
|
481
|
+
process.env.GEMINI_DISABLE_HISTORY_DISCOVERY === "1");
|
|
218
482
|
}
|
|
219
|
-
function
|
|
220
|
-
|
|
483
|
+
function parsePositiveInt(value, fallback) {
|
|
484
|
+
if (!value) {
|
|
485
|
+
return fallback;
|
|
486
|
+
}
|
|
487
|
+
const parsed = Number.parseInt(value, 10);
|
|
488
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
221
489
|
}
|
|
222
490
|
function collectGeminiModels() {
|
|
223
|
-
const root = path.join(homedir(), ".gemini", "tmp");
|
|
491
|
+
const root = process.env.GEMINI_HISTORY_ROOT || path.join(homedir(), ".gemini", "tmp");
|
|
224
492
|
if (!existsSync(root)) {
|
|
225
|
-
return
|
|
493
|
+
return [];
|
|
226
494
|
}
|
|
495
|
+
const maxFiles = parsePositiveInt(process.env.GEMINI_HISTORY_MAX_FILES, MAX_GEMINI_HISTORY_FILES);
|
|
496
|
+
const maxFileBytes = parsePositiveInt(process.env.GEMINI_HISTORY_MAX_FILE_BYTES, MAX_GEMINI_HISTORY_FILE_BYTES);
|
|
227
497
|
const candidates = [];
|
|
228
498
|
const roots = safeReadDir(root);
|
|
229
499
|
roots.forEach(entry => {
|
|
@@ -241,7 +511,9 @@ function collectGeminiModels() {
|
|
|
241
511
|
const filePath = path.join(chatsPath, file.name);
|
|
242
512
|
try {
|
|
243
513
|
const stat = statSync(filePath);
|
|
244
|
-
|
|
514
|
+
if (stat.size <= maxFileBytes) {
|
|
515
|
+
candidates.push({ filePath, mtimeMs: stat.mtimeMs, size: stat.size });
|
|
516
|
+
}
|
|
245
517
|
}
|
|
246
518
|
catch {
|
|
247
519
|
return;
|
|
@@ -249,43 +521,69 @@ function collectGeminiModels() {
|
|
|
249
521
|
});
|
|
250
522
|
});
|
|
251
523
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
252
|
-
const maxFiles = 200;
|
|
253
524
|
const recent = candidates.slice(0, maxFiles);
|
|
254
525
|
const models = {};
|
|
255
526
|
for (const candidate of recent) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
527
|
+
extractGeminiModels(candidate.filePath).forEach(model => {
|
|
528
|
+
const existing = models[model]?.lastSeenMs ?? 0;
|
|
529
|
+
if (candidate.mtimeMs > existing) {
|
|
530
|
+
models[model] = {
|
|
531
|
+
model,
|
|
532
|
+
lastSeen: formatDate(candidate.mtimeMs),
|
|
533
|
+
lastSeenMs: candidate.mtimeMs,
|
|
534
|
+
filePath: candidate.filePath,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
});
|
|
264
538
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const versionDiff = extractModelVersion(b[0]) - extractModelVersion(a[0]);
|
|
539
|
+
return Object.values(models).sort((a, b) => {
|
|
540
|
+
const versionDiff = extractModelVersion(b.model) - extractModelVersion(a.model);
|
|
268
541
|
if (versionDiff !== 0) {
|
|
269
542
|
return versionDiff;
|
|
270
543
|
}
|
|
271
|
-
return b
|
|
272
|
-
})
|
|
273
|
-
.map(([name]) => name);
|
|
274
|
-
const describedModels = {};
|
|
275
|
-
order.forEach(model => {
|
|
276
|
-
describedModels[model] = `Observed in local Gemini sessions (last seen ${formatDate(models[model].lastSeen)})`;
|
|
544
|
+
return b.lastSeenMs - a.lastSeenMs;
|
|
277
545
|
});
|
|
278
|
-
return { models: describedModels, order };
|
|
279
546
|
}
|
|
280
|
-
function
|
|
547
|
+
function extractGeminiModels(filePath) {
|
|
281
548
|
try {
|
|
282
549
|
const content = readFileSync(filePath, "utf-8");
|
|
283
|
-
const
|
|
284
|
-
|
|
550
|
+
const found = new Set();
|
|
551
|
+
try {
|
|
552
|
+
collectModelValues(JSON.parse(content), found);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
const regex = /"model(?:Name|Id)?"\s*:\s*"([^"]+)"/g;
|
|
556
|
+
let match;
|
|
557
|
+
while ((match = regex.exec(content)) !== null) {
|
|
558
|
+
found.add(match[1]);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return [...found].filter(isValidModelName).slice(0, 20);
|
|
285
562
|
}
|
|
286
563
|
catch {
|
|
287
|
-
return
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function collectModelValues(value, found) {
|
|
568
|
+
if (!value || found.size >= 20) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (Array.isArray(value)) {
|
|
572
|
+
value.forEach(entry => collectModelValues(entry, found));
|
|
573
|
+
return;
|
|
288
574
|
}
|
|
575
|
+
if (typeof value !== "object") {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
579
|
+
if ((key === "model" || key === "modelName" || key === "modelId") &&
|
|
580
|
+
typeof child === "string") {
|
|
581
|
+
found.add(child);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
collectModelValues(child, found);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
289
587
|
}
|
|
290
588
|
function formatDate(timestampMs) {
|
|
291
589
|
try {
|
|
@@ -336,11 +634,21 @@ function buildOrder(info, preferred) {
|
|
|
336
634
|
}
|
|
337
635
|
function pickLatestMatching(info, token) {
|
|
338
636
|
const normalized = token.toLowerCase();
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
637
|
+
const candidates = Object.keys(info.models)
|
|
638
|
+
.filter(model => model.toLowerCase().includes(normalized))
|
|
639
|
+
.sort((a, b) => {
|
|
640
|
+
const versionDiff = extractModelVersion(b) - extractModelVersion(a);
|
|
641
|
+
if (versionDiff !== 0) {
|
|
642
|
+
return versionDiff;
|
|
343
643
|
}
|
|
344
|
-
|
|
345
|
-
|
|
644
|
+
const aMetadata = info.modelMetadata?.[a];
|
|
645
|
+
const bMetadata = info.modelMetadata?.[b];
|
|
646
|
+
const priorityDiff = (bMetadata ? SOURCE_PRIORITY[bMetadata.source] : 0) -
|
|
647
|
+
(aMetadata ? SOURCE_PRIORITY[aMetadata.source] : 0);
|
|
648
|
+
if (priorityDiff !== 0) {
|
|
649
|
+
return priorityDiff;
|
|
650
|
+
}
|
|
651
|
+
return a.localeCompare(b);
|
|
652
|
+
});
|
|
653
|
+
return candidates[0];
|
|
346
654
|
}
|