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