llm-cli-gateway 1.5.15 → 1.5.17
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 +18 -1
- package/README.md +5 -4
- package/dist/async-job-manager.js +16 -1
- package/dist/executor.js +11 -4
- package/dist/index.js +55 -6
- package/dist/model-registry.js +112 -13
- package/dist/upstream-contracts.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the llm-cli-gateway project.
|
|
4
4
|
|
|
5
|
+
## [1.5.17] - 2026-05-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Make desktop bootstrapper `doctor --json` delegate to the installed Node gateway doctor when a verified bundle is installed, so provider availability and `gateway.version` reflect the active bundle instead of stale bootstrapper-side placeholders.
|
|
10
|
+
- Add `gateway.bootstrapper_version` and `gateway.diagnostic_source` to desktop doctor output so bundle version and bootstrapper version are distinguishable.
|
|
11
|
+
- Include `bootstrapper_version` in desktop `upgrade` output and make the post-upgrade note explicit that command fixes require replacing the bootstrapper executable.
|
|
12
|
+
|
|
13
|
+
## [1.5.16] - 2026-05-24
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Remove the stale hardcoded Mistral Vibe `devstral-medium` default from the gateway request path.
|
|
18
|
+
- Discover Mistral Vibe model aliases from `~/.vibe/config.toml`, `VIBE_MODELS`, `VIBE_ACTIVE_MODEL`, and gateway env overrides before injecting `VIBE_ACTIVE_MODEL`.
|
|
19
|
+
- Recover stale Vibe config such as `active_model = "devstral-medium"` to `mistral-medium-3.5`, and retry one synchronous Mistral request after a model-not-found failure with refreshed discovery.
|
|
20
|
+
- Build provider CLI PATH values with the platform delimiter so Windows desktop installs can find CLIs in locations such as `%USERPROFILE%\.local\bin`, and normalize Windows `-4058` launch failures to command-not-found guidance.
|
|
21
|
+
|
|
5
22
|
## [1.5.15] - 2026-05-24
|
|
6
23
|
|
|
7
24
|
### Fixed
|
|
@@ -134,7 +151,7 @@ Lands DAG layers 6-12 — the personal-MCP MVP terminal plus all of Phase 0-3 pr
|
|
|
134
151
|
- **U13 / U16 — Release packaging + dogfood readiness.** `installer/build-release.sh` cross-compiles 5 OS/arch targets (linux/{amd64,arm64}, darwin/{amd64,arm64}, windows/amd64) + Node bundle + `SHA256SUMS` + `release-manifest.json`. New `cli_upgrade --uninstall` (idempotent, dry-run by default) and `cli_upgrade --check`. New `Dockerfile.personal` + `docker-compose.personal.yml` for the personal-MCP container path. New `installer/packaging/README.md`. New `package.json` scripts `release:build`, `release:checksums`, `release:docker`. Comprehensive `docs/personal-mcp/{DOGFOODING_RESULTS,RELEASE_READINESS,SINGLE_BINARY_INSTALLER,ENDPOINT_EXPOSURE,PRODUCT_CONTRACT,PROVIDER_SUPPORT_MATRIX,VALIDATION_REPORT_FORMAT}.md` + per-provider `connect-*.md` guides + `setup/assistants/*-install-prompt.md` install-prompt corpus.
|
|
135
152
|
- **U21 — Phase-0 parity fixes.** `SESSION_PROVIDER_VALUES` / `SESSION_PROVIDER_ENUM` now expose the full provider set (grok was previously absent from `session_create`/`session_list`/`session_clear_all` Zod enums despite the storage layer supporting it). `prepareGeminiRequest` emits `["-p", prompt, ...]` instead of a positional prompt, eliminating the dependency on Gemini's TTY/mode-detection heuristics. 6 new tests pin both fixes.
|
|
136
153
|
- **U22 — Mistral Vibe is the fifth supported provider.** New `mistral_request` and `mistral_request_async` MCP tools register alongside the four incumbents and route through the same async job manager, dedup store, flight recorder, approval manager, and validation orchestrator. Five Vibe-specific divergences are documented in `docs/personal-mcp/PROVIDER_MODERNISATION_AUDIT.md`:
|
|
137
|
-
- **No `--model` flag** — model selection is via the `VIBE_ACTIVE_MODEL` environment variable
|
|
154
|
+
- **No `--model` flag** — model selection is via the `VIBE_ACTIVE_MODEL` environment variable; the gateway discovers Vibe config/env models, avoids stale hardcoded defaults, and forwards an `env` override only when needed.
|
|
138
155
|
- **Session-logging is opt-in** in `~/.vibe/config.toml` — `doctor --json` probes `[session_logging] enabled = true` (read-only) and surfaces an actionable `next_actions` entry when the toggle is missing.
|
|
139
156
|
- **`--agent` enum** replaces Grok's `--always-approve` (`default | plan | accept-edits | auto-approve | chat | explore | lean`); the gateway always emits `--agent` explicitly and defaults to `auto-approve` for programmatic callers.
|
|
140
157
|
- **`--enabled-tools` allow-list only** — `allowedTools` emits one `--enabled-tools <tool>` per entry; `disallowedTools` is accepted in the schema for caller parity but silently ignored at the CLI boundary (a logged warning records the no-op).
|
package/README.md
CHANGED
|
@@ -149,9 +149,10 @@ vibe config set session_logging.enabled true # or edit ~/.vibe/config.toml
|
|
|
149
149
|
Vibe-specific notes:
|
|
150
150
|
|
|
151
151
|
- **Model selection is via the `VIBE_ACTIVE_MODEL` environment variable** —
|
|
152
|
-
Vibe has no `--model` flag. The gateway
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
Vibe has no `--model` flag. The gateway discovers `~/.vibe/config.toml` /
|
|
153
|
+
`VIBE_MODELS`, injects `VIBE_ACTIVE_MODEL` only when a model is explicitly
|
|
154
|
+
requested or Vibe config needs recovery, and retries once after a
|
|
155
|
+
model-not-found failure with refreshed discovery.
|
|
155
156
|
- **`permissionMode` accepts** `default | plan | accept-edits | auto-approve |
|
|
156
157
|
chat | explore | lean` and emits `--agent <mode>`. The gateway's
|
|
157
158
|
programmatic-mode default is `auto-approve`; pick a stricter mode
|
|
@@ -594,7 +595,7 @@ consumes = ["OUT:mcp-reconnected"]
|
|
|
594
595
|
##### `mistral_request`
|
|
595
596
|
Run a Mistral Vibe agentic coding request. Like `grok_request` in shape, but with Vibe's specific surface:
|
|
596
597
|
|
|
597
|
-
- `model` (string, optional):
|
|
598
|
+
- `model` (string, optional): Vibe model alias (for example `mistral-medium-3.5` or `latest`). The resolved value is injected via the `VIBE_ACTIVE_MODEL` environment variable; omit it to let the gateway discover Vibe config and avoid stale hardcoded defaults.
|
|
598
599
|
- `permissionMode`: `default | plan | accept-edits | auto-approve | chat | explore | lean` — emitted as `--agent <mode>`. Defaults to `auto-approve` in programmatic mode.
|
|
599
600
|
- `allowedTools` (string[], optional): One `--enabled-tools <tool>` flag per entry (allow-list only).
|
|
600
601
|
- `disallowedTools` (string[], optional): Accepted for parity with the other providers; ignored at the CLI boundary with a logged warning.
|
|
@@ -20,6 +20,15 @@ function describeProcessLaunchError(cli, error) {
|
|
|
20
20
|
message: `Failed to launch ${cli} CLI: ${error.message}`,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
function describeWindowsLaunchExit(cli, exitCode) {
|
|
24
|
+
if (exitCode !== -4058) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
exitCode: 127,
|
|
29
|
+
message: `The '${cli}' command was not found. Install the ${cli} CLI and make sure it is on PATH.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
23
32
|
/**
|
|
24
33
|
* U22 fix: deterministic canonicalisation of an env-var map for the dedup key.
|
|
25
34
|
* Returns "" when env is undefined or empty (preserves dedup key continuity for
|
|
@@ -483,7 +492,13 @@ export class AsyncJobManager {
|
|
|
483
492
|
this.fireOnComplete(job);
|
|
484
493
|
return;
|
|
485
494
|
}
|
|
486
|
-
|
|
495
|
+
const rawExitCode = code ?? 0;
|
|
496
|
+
const launchExit = !job.stdout && !job.stderr ? describeWindowsLaunchExit(cli, rawExitCode) : null;
|
|
497
|
+
job.exitCode = launchExit?.exitCode ?? rawExitCode;
|
|
498
|
+
if (launchExit) {
|
|
499
|
+
job.error = launchExit.message;
|
|
500
|
+
job.stderr = launchExit.message;
|
|
501
|
+
}
|
|
487
502
|
job.finishedAt = new Date().toISOString();
|
|
488
503
|
if (job.canceled) {
|
|
489
504
|
job.status = "canceled";
|
package/dist/executor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "child_process";
|
|
2
2
|
import { homedir } from "os";
|
|
3
|
-
import { join, dirname } from "path";
|
|
3
|
+
import { delimiter, join, dirname } from "path";
|
|
4
4
|
import { readdirSync, existsSync } from "fs";
|
|
5
5
|
import { createCircuitBreaker, withRetry } from "./retry.js";
|
|
6
6
|
const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
|
|
@@ -28,7 +28,7 @@ function getNvmPath() {
|
|
|
28
28
|
try {
|
|
29
29
|
const versions = readdirSync(nvmVersionsDir);
|
|
30
30
|
cachedNvmPath = versions.length
|
|
31
|
-
? versions.map(version => join(nvmVersionsDir, version, "bin")).join(
|
|
31
|
+
? versions.map(version => join(nvmVersionsDir, version, "bin")).join(delimiter)
|
|
32
32
|
: null;
|
|
33
33
|
}
|
|
34
34
|
catch {
|
|
@@ -51,7 +51,7 @@ export function getExtendedPath() {
|
|
|
51
51
|
additionalPaths.push(nvmPath);
|
|
52
52
|
}
|
|
53
53
|
const currentPath = process.env.PATH || "";
|
|
54
|
-
return [...additionalPaths, currentPath].join(
|
|
54
|
+
return [...additionalPaths, currentPath].join(delimiter);
|
|
55
55
|
}
|
|
56
56
|
/** Registry of active detached process groups for shutdown cleanup. */
|
|
57
57
|
const activeProcessGroups = new Set();
|
|
@@ -315,7 +315,14 @@ export async function executeCli(command, args, options = {}) {
|
|
|
315
315
|
reject(error);
|
|
316
316
|
return;
|
|
317
317
|
}
|
|
318
|
-
|
|
318
|
+
let result = { stdout, stderr, code: code ?? 0 };
|
|
319
|
+
if (result.code === -4058 && !stdout && !stderr) {
|
|
320
|
+
result = {
|
|
321
|
+
stdout,
|
|
322
|
+
stderr: `The '${command}' command was not found. Install the ${command} CLI and make sure it is on PATH.`,
|
|
323
|
+
code: 127,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
319
326
|
if (result.code !== 0) {
|
|
320
327
|
const error = new Error(`Process exited with code ${result.code}`);
|
|
321
328
|
error.code = result.code;
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { PerformanceMetrics } from "./metrics.js";
|
|
|
16
16
|
import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
|
|
17
17
|
import { loadConfig, loadPersistenceConfig } from "./config.js";
|
|
18
18
|
import { checkHealth } from "./health.js";
|
|
19
|
-
import { getCliInfo, resolveModelAlias } from "./model-registry.js";
|
|
19
|
+
import { clearModelRegistryCache, getCliInfo, resolveModelAlias } from "./model-registry.js";
|
|
20
20
|
import { AsyncJobManager } from "./async-job-manager.js";
|
|
21
21
|
import { createJobStore } from "./job-store.js";
|
|
22
22
|
import { ApprovalManager } from "./approval-manager.js";
|
|
@@ -320,6 +320,7 @@ function buildDeferredToolResponse(deferred, sessionId) {
|
|
|
320
320
|
// Helper function for standardized error responses
|
|
321
321
|
function createErrorResponse(cli, code, stderr, correlationId, error) {
|
|
322
322
|
let errorMessage = `Error executing ${cli} CLI`;
|
|
323
|
+
const isLaunchExit = code === 127 || code === -4058;
|
|
323
324
|
if (error) {
|
|
324
325
|
// Command not found or spawn error
|
|
325
326
|
errorMessage += `:\n${error.message}`;
|
|
@@ -338,6 +339,10 @@ function createErrorResponse(cli, code, stderr, correlationId, error) {
|
|
|
338
339
|
errorMessage += `: Process killed due to inactivity\n${stderr}`;
|
|
339
340
|
logger.error(`[${correlationId || "unknown"}] ${cli} CLI killed due to inactivity`);
|
|
340
341
|
}
|
|
342
|
+
else if (isLaunchExit) {
|
|
343
|
+
errorMessage += `:\n${stderr || `The '${cli}' command was not found. Install the ${cli} CLI and make sure it is on PATH.`}`;
|
|
344
|
+
logger.error(`[${correlationId || "unknown"}] ${cli} CLI failed to launch`);
|
|
345
|
+
}
|
|
341
346
|
else if (code !== 0) {
|
|
342
347
|
// Other non-zero exit code
|
|
343
348
|
errorMessage += ` (exit code ${code}):\n${stderr}`;
|
|
@@ -356,7 +361,9 @@ function createErrorResponse(cli, code, stderr, correlationId, error) {
|
|
|
356
361
|
? "idle_timeout"
|
|
357
362
|
: error
|
|
358
363
|
? "spawn_error"
|
|
359
|
-
:
|
|
364
|
+
: isLaunchExit
|
|
365
|
+
? "spawn_error"
|
|
366
|
+
: "cli_error",
|
|
360
367
|
},
|
|
361
368
|
};
|
|
362
369
|
}
|
|
@@ -1057,7 +1064,8 @@ function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime()) {
|
|
|
1057
1064
|
function prepareMistralRequest(params, runtime = resolveGatewayServerRuntime()) {
|
|
1058
1065
|
const corrId = params.correlationId || randomUUID();
|
|
1059
1066
|
const cliInfo = getCliInfo();
|
|
1060
|
-
const
|
|
1067
|
+
const requestedModel = params.model ?? (cliInfo.mistral.defaultModel ? "default" : undefined);
|
|
1068
|
+
const resolvedModel = resolveModelAlias("mistral", requestedModel, cliInfo);
|
|
1061
1069
|
const reviewIntegrity = checkReviewIntegrity({
|
|
1062
1070
|
prompt: params.prompt,
|
|
1063
1071
|
allowedTools: params.allowedTools,
|
|
@@ -1089,7 +1097,10 @@ function prepareMistralRequest(params, runtime = resolveGatewayServerRuntime())
|
|
|
1089
1097
|
allowedTools: params.allowedTools,
|
|
1090
1098
|
disallowedTools: params.disallowedTools,
|
|
1091
1099
|
policy: params.approvalPolicy,
|
|
1092
|
-
metadata: {
|
|
1100
|
+
metadata: {
|
|
1101
|
+
model: resolvedModel ?? "vibe-default",
|
|
1102
|
+
vibeActiveModelEnv: Boolean(resolvedModel),
|
|
1103
|
+
},
|
|
1093
1104
|
reviewIntegrity,
|
|
1094
1105
|
});
|
|
1095
1106
|
if (approvalDecision.status !== "approved") {
|
|
@@ -1126,6 +1137,19 @@ function prepareMistralRequest(params, runtime = resolveGatewayServerRuntime())
|
|
|
1126
1137
|
mistralEnv: prep.env,
|
|
1127
1138
|
};
|
|
1128
1139
|
}
|
|
1140
|
+
function isMistralModelSelectionFailure(stderr) {
|
|
1141
|
+
return /active model ['"].+['"] not found|model ['"].+['"] (?:isn't|is not) found|unknown model|model not found/i.test(stderr);
|
|
1142
|
+
}
|
|
1143
|
+
function selectMistralRecoveryModel(failedModel) {
|
|
1144
|
+
clearModelRegistryCache();
|
|
1145
|
+
const refreshed = getCliInfo(true).mistral;
|
|
1146
|
+
const candidates = [
|
|
1147
|
+
refreshed.defaultModel,
|
|
1148
|
+
...(refreshed.modelOrder ?? []),
|
|
1149
|
+
...Object.keys(refreshed.models),
|
|
1150
|
+
].filter((model) => Boolean(model && model !== failedModel));
|
|
1151
|
+
return candidates.find(model => model !== "local");
|
|
1152
|
+
}
|
|
1129
1153
|
function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep, durationMs, resumable, outputFormat) {
|
|
1130
1154
|
let finalStdout = stdout;
|
|
1131
1155
|
// Skip response optimization for JSON output to prevent corrupting structured data
|
|
@@ -1634,10 +1658,35 @@ export async function handleMistralRequest(deps, params) {
|
|
|
1634
1658
|
createNewSession: params.createNewSession,
|
|
1635
1659
|
});
|
|
1636
1660
|
args.push(...sessionResult.resumeArgs);
|
|
1637
|
-
|
|
1661
|
+
let result = await awaitJobOrDefer("mistral", args, corrId, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, mistralEnv);
|
|
1638
1662
|
if (isDeferredResponse(result)) {
|
|
1639
1663
|
return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
|
|
1640
1664
|
}
|
|
1665
|
+
if (result.code !== 0 && isMistralModelSelectionFailure(result.stderr)) {
|
|
1666
|
+
const recoveryModel = selectMistralRecoveryModel(prep.resolvedModel);
|
|
1667
|
+
if (recoveryModel) {
|
|
1668
|
+
deps.logger.info(`[${corrId}] mistral_request detected stale Vibe model selection; retrying once with ${recoveryModel}`);
|
|
1669
|
+
const retryPrep = buildMistralCliInvocation({
|
|
1670
|
+
prompt: prep.effectivePrompt,
|
|
1671
|
+
resolvedModel: recoveryModel,
|
|
1672
|
+
outputFormat: params.outputFormat,
|
|
1673
|
+
permissionMode: params.approvalStrategy === "mcp_managed"
|
|
1674
|
+
? "auto-approve"
|
|
1675
|
+
: (params.permissionMode ?? "auto-approve"),
|
|
1676
|
+
effort: params.effort,
|
|
1677
|
+
reasoningEffort: params.reasoningEffort,
|
|
1678
|
+
allowedTools: params.allowedTools,
|
|
1679
|
+
disallowedTools: params.disallowedTools,
|
|
1680
|
+
});
|
|
1681
|
+
const retryArgs = [...retryPrep.args, ...sessionResult.resumeArgs];
|
|
1682
|
+
result = await awaitJobOrDefer("mistral", retryArgs, corrId, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, true, runtime, retryPrep.env);
|
|
1683
|
+
if (isDeferredResponse(result)) {
|
|
1684
|
+
return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
|
|
1685
|
+
}
|
|
1686
|
+
prep.resolvedModel = recoveryModel;
|
|
1687
|
+
prep.args = retryArgs;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1641
1690
|
const { stdout, stderr, code } = result;
|
|
1642
1691
|
durationMs = Math.max(0, Date.now() - startTime);
|
|
1643
1692
|
if (code !== 0) {
|
|
@@ -2688,7 +2737,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
2688
2737
|
model: z
|
|
2689
2738
|
.string()
|
|
2690
2739
|
.optional()
|
|
2691
|
-
.describe("Model alias (e.g.
|
|
2740
|
+
.describe("Model alias (e.g. mistral-medium-3.5, latest). Resolved alias is injected via VIBE_ACTIVE_MODEL env var; Vibe has no --model flag."),
|
|
2692
2741
|
outputFormat: z
|
|
2693
2742
|
.enum(["plain", "json", "stream-json"])
|
|
2694
2743
|
.optional()
|
package/dist/model-registry.js
CHANGED
|
@@ -45,18 +45,16 @@ const FALLBACK_INFO = {
|
|
|
45
45
|
modelOrder: ["grok-build"],
|
|
46
46
|
},
|
|
47
47
|
mistral: {
|
|
48
|
-
// Mistral Vibe selects the active model via
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
48
|
+
// Mistral Vibe selects the active model via VIBE_ACTIVE_MODEL; there is no
|
|
49
|
+
// `--model` flag. Do not set a bundled default here: Vibe's own default and
|
|
50
|
+
// user config move independently of this gateway. The model list is only a
|
|
51
|
+
// low-confidence recovery set for stale config/model-not-found failures.
|
|
52
52
|
description: "Mistral AI's Vibe CLI - agentic coding via Mistral models (model selection via VIBE_ACTIVE_MODEL env var)",
|
|
53
53
|
models: {
|
|
54
|
-
"
|
|
55
|
-
"devstral-
|
|
56
|
-
"mistral-large-latest": "General-purpose flagship Mistral model. Best for: non-Devstral reasoning workloads",
|
|
54
|
+
"mistral-medium-3.5": "Vibe coding model alias observed in Vibe 2.x defaults. Used only when discovery/config requires an explicit VIBE_ACTIVE_MODEL.",
|
|
55
|
+
"devstral-small": "Vibe coding model alias observed in Vibe 2.x defaults. Used only when configured explicitly.",
|
|
57
56
|
},
|
|
58
|
-
|
|
59
|
-
modelOrder: ["devstral-medium", "devstral-large", "mistral-large-latest"],
|
|
57
|
+
modelOrder: ["mistral-medium-3.5", "devstral-small"],
|
|
60
58
|
},
|
|
61
59
|
};
|
|
62
60
|
const MODEL_CACHE_TTL_MS = 2 * 60 * 1000;
|
|
@@ -341,21 +339,116 @@ function applyGrokOverrides(info) {
|
|
|
341
339
|
info.modelOrder = buildOrder(info, info.defaultModel);
|
|
342
340
|
}
|
|
343
341
|
function applyMistralOverrides(info) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const envDefault = process.env.MISTRAL_DEFAULT_MODEL || process.env.VIBE_ACTIVE_MODEL;
|
|
342
|
+
const vibeConfig = readVibeConfig(info);
|
|
343
|
+
addVibeModelEntries(info, parseVibeModels(process.env.VIBE_MODELS, "VIBE_MODELS"), "env");
|
|
344
|
+
addVibeModelEntries(info, vibeConfig.models, "config");
|
|
348
345
|
addEnvModels(info, "MISTRAL_MODELS");
|
|
349
346
|
addEnvAliases(info, "mistral", "MISTRAL_MODEL_ALIASES");
|
|
350
347
|
addGlobalEnvAliases(info, "mistral");
|
|
348
|
+
// Vibe uses VIBE_ACTIVE_MODEL instead of a CLI flag. Explicit env values win.
|
|
349
|
+
const envDefault = process.env.MISTRAL_DEFAULT_MODEL || process.env.VIBE_ACTIVE_MODEL;
|
|
351
350
|
if (envDefault) {
|
|
352
351
|
const source = process.env.MISTRAL_DEFAULT_MODEL
|
|
353
352
|
? "MISTRAL_DEFAULT_MODEL"
|
|
354
353
|
: "VIBE_ACTIVE_MODEL";
|
|
355
354
|
setDefaultModel(info, envDefault, source, "env");
|
|
356
355
|
}
|
|
356
|
+
else if (vibeConfig.activeModel) {
|
|
357
|
+
const configuredActiveModel = vibeConfig.activeModel;
|
|
358
|
+
if (info.models[configuredActiveModel]) {
|
|
359
|
+
setDefaultModel(info, configuredActiveModel, vibeConfig.source, "config");
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
const migrated = migrateDeprecatedMistralModel(configuredActiveModel, info);
|
|
363
|
+
if (migrated) {
|
|
364
|
+
addWarning(info, `Vibe active_model '${configuredActiveModel}' is not in the discovered model list; using '${migrated}' as the gateway recovery model`);
|
|
365
|
+
setDefaultModel(info, migrated, `${vibeConfig.source} recovery`, "config");
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
addWarning(info, `Vibe active_model '${configuredActiveModel}' is not in the discovered model list`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
357
372
|
info.modelOrder = buildOrder(info, info.defaultModel);
|
|
358
373
|
}
|
|
374
|
+
function readVibeConfig(info) {
|
|
375
|
+
const vibeHome = process.env.VIBE_HOME || path.join(homedir(), ".vibe");
|
|
376
|
+
const configPath = path.join(vibeHome, "config.toml");
|
|
377
|
+
const result = { models: [], source: configPath };
|
|
378
|
+
if (!existsSync(configPath)) {
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const parsed = parseToml(readFileSync(configPath, "utf-8"));
|
|
383
|
+
const activeModel = readStringProperty(parsed, "active_model");
|
|
384
|
+
if (activeModel) {
|
|
385
|
+
result.activeModel = activeModel.trim();
|
|
386
|
+
}
|
|
387
|
+
result.models = parseVibeModelArray(readRecordOrArrayProperty(parsed, "models"), configPath);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
391
|
+
addWarning(info, `Could not parse Vibe config ${configPath}: ${message}`);
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
function parseVibeModels(value, sourceDetail) {
|
|
396
|
+
if (!value || !value.trim()) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
return parseVibeModelArray(JSON.parse(value), sourceDetail);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function parseVibeModelArray(value, sourceDetail) {
|
|
407
|
+
if (!Array.isArray(value)) {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
return value
|
|
411
|
+
.map((entry) => {
|
|
412
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const record = entry;
|
|
416
|
+
const alias = typeof record.alias === "string" ? record.alias.trim() : "";
|
|
417
|
+
const name = typeof record.name === "string" ? record.name.trim() : undefined;
|
|
418
|
+
if (!alias) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
alias,
|
|
423
|
+
name,
|
|
424
|
+
description: name ? `${alias} (${name}) from Vibe config` : `${alias} from Vibe config`,
|
|
425
|
+
sourceDetail,
|
|
426
|
+
};
|
|
427
|
+
})
|
|
428
|
+
.filter((entry) => entry !== null);
|
|
429
|
+
}
|
|
430
|
+
function addVibeModelEntries(info, entries, source) {
|
|
431
|
+
entries.forEach(entry => {
|
|
432
|
+
addModel(info, entry.alias, entry.description ?? `${entry.alias} from Vibe model configuration`, {
|
|
433
|
+
source,
|
|
434
|
+
sourceDetail: entry.sourceDetail,
|
|
435
|
+
confidence: source === "env" ? "high" : "medium",
|
|
436
|
+
}, { preferDescription: true });
|
|
437
|
+
if (entry.name && entry.name !== entry.alias && isValidModelName(entry.name)) {
|
|
438
|
+
addAlias(info, entry.name, entry.alias, entry.sourceDetail);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function migrateDeprecatedMistralModel(model, info) {
|
|
443
|
+
const normalized = model.trim().toLowerCase();
|
|
444
|
+
const replacement = ["devstral-medium", "devstral-large"].includes(normalized)
|
|
445
|
+
? "mistral-medium-3.5"
|
|
446
|
+
: undefined;
|
|
447
|
+
if (replacement && info.models[replacement]) {
|
|
448
|
+
return replacement;
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
359
452
|
function readJsonStringValue(filePath, paths, info) {
|
|
360
453
|
if (!existsSync(filePath)) {
|
|
361
454
|
return undefined;
|
|
@@ -405,6 +498,12 @@ function readRecordProperty(value, key) {
|
|
|
405
498
|
? prop
|
|
406
499
|
: {};
|
|
407
500
|
}
|
|
501
|
+
function readRecordOrArrayProperty(value, key) {
|
|
502
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
return value[key];
|
|
506
|
+
}
|
|
408
507
|
function parseEnvModelEntries(value, source) {
|
|
409
508
|
if (!value) {
|
|
410
509
|
return [];
|
|
@@ -368,7 +368,7 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
368
368
|
id: "mistral-minimal",
|
|
369
369
|
description: "Minimal prompt request with env-selected model",
|
|
370
370
|
args: ["-p", "hello", "--agent", "auto-approve"],
|
|
371
|
-
env: { VIBE_ACTIVE_MODEL: "
|
|
371
|
+
env: { VIBE_ACTIVE_MODEL: "mistral-medium-3.5" },
|
|
372
372
|
expect: "pass",
|
|
373
373
|
},
|
|
374
374
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.17",
|
|
4
4
|
"mcpName": "io.github.verivus-oss/llm-cli-gateway",
|
|
5
5
|
"description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
|
|
6
6
|
"license": "MIT",
|