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 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 (default alias: `devstral-medium`); the executor and async job manager forward an `env` override.
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 resolves the requested model alias
153
- (default: `devstral-medium`) and injects it as `VIBE_ACTIVE_MODEL` when
154
- spawning `vibe`.
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): Resolved alias (e.g. `devstral-medium`, `devstral-large`, `latest`). The resolved value is injected via the `VIBE_ACTIVE_MODEL` environment variable Vibe has no `--model` flag.
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
- job.exitCode = code ?? 0;
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
- const result = { stdout, stderr, code: code ?? 0 };
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
- : "cli_error",
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 resolvedModel = resolveModelAlias("mistral", params.model, cliInfo) || "devstral-medium";
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: { model: resolvedModel, vibeActiveModelEnv: true },
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
- const result = await awaitJobOrDefer("mistral", args, corrId, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, mistralEnv);
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. devstral-medium, devstral-large, latest). Resolved alias is injected via VIBE_ACTIVE_MODEL env var Vibe has no --model flag."),
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()
@@ -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 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.
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
- "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",
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
- defaultModel: "devstral-medium",
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
- // 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;
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: "devstral-medium" },
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.15",
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",