Files
olp/lib/providers/index.mjs
T
taodengandClaude Opus 4.7 (noreply@anthropic.com) 95dc072245 feat(phase-1): land Mistral Vibe provider plugin (D8)
Phase 1 Day 5. Mistral Vibe provider plugin lands as Candidate. STATIC_
REGISTRY now length 3 (anthropic + openai + mistral). vibe CLI not
installed on the orchestrator machine and owner has no Le Chat Pro
subscription, so D8 follows the D6 docs-only authority pattern. D-later
verification (once a tester with a vibe install and subscription is
available) will resolve UNPINNED assumptions A4, A6, A7, A8.

Files:
  NEW:  lib/providers/mistral.mjs (~720 lines) — Mistral Vibe provider.
        Spawns `vibe --prompt PROMPT --output streaming` per docs.
        Reads MISTRAL_API_KEY env var with ~/.vibe/.env fallback.
        Supports VIBE_HOME env override per docs. Mirrors D4/D6 plugin
        structure incl. __setSpawnImpl/__resetSpawnImpl for tests.
  MOD:  lib/providers/index.mjs (+12/-1) — STATIC_REGISTRY now length 3.
        listAllProviderNames returns [anthropic, openai, mistral].
  MOD:  models-registry.json (+26 lines) — providers.mistral with 2
        canonical date-stamped IDs (devstral-2-25-12, devstral-small-2-
        25-12) and 4 short-form aliases for user convenience.
  MOD:  test-features.mjs — Suite 12 with 48 tests covering contract
        conformance, IR translation, mock-spawn behaviour, healthCheck,
        estimateCost, auth-artifact reading.

Authority citations (all WebFetched and verified during reviewer pass):
  DOCS-1: docs.mistral.ai/mistral-vibe/terminal/quickstart — `vibe
    --prompt PROMPT --max-turns 5 --max-price 1.0 --output json` example
    + Output Format Options enumeration: text default, json single blob,
    streaming NDJSON.
  DOCS-2: docs.mistral.ai/mistral-vibe/terminal/configuration — pin
    `~/.vibe/.env`, MISTRAL_API_KEY env var, VIBE_HOME override.
  DOCS-3: docs.mistral.ai/mistral-vibe/introduction/configuration —
    second confirmation of MISTRAL_API_KEY + ~/.vibe/.env (model
    selection via config.toml `/config` slash command, NOT --model
    flag).
  DOCS-4: deepwiki.com/mistralai/mistral-vibe/9.3-cli-commands-reference
    — full flag enumeration confirms no --model flag exists. Programmatic
    mode trigger is --prompt; flags are: --continue, --max-price,
    --max-turns, --output, --prompt, --resume, --setup, --trust,
    --upgrade, --version, --workdir, --no-autofill, --no-header,
    --no-dev, --enabled-tools, --help.
  DOCS-5: mistral.ai/news/devstral-2-vibe-cli — launch announcement,
    names Devstral 2 (123B) and Devstral Small 2 (24B), 256K context,
    pricing $0.40/$2.00 and $0.10/$0.30 per MTok.
  DOCS-6: help.mistral.ai/en/articles/347532 — Vibe included in Le Chat
    Pro.
  DOCS-7: legal.mistral.ai/terms/usage-policy — no anti-third-party
    clauses; ADR 0006 Tier D classification holds.
  DOCS-MAIN: docs.mistral.ai/mistral-vibe/overview — main Vibe overview.
  DOCS-8: docs.mistral.ai/getting-started/models/models_overview —
    canonical Mistral models registry. Pin for date-stamped IDs
    devstral-2-25-12 / devstral-small-2-25-12. Caught by D8 review-2.

Architectural decisions:
  1. `--output streaming` (NOT `json`). Per DOCS-1 verbatim: streaming
     emits NDJSON per message; json emits a single blob at the end.
     Original D8 draft used `json` which is incompatible with the
     plugin's line-buffered stdout parser. Review-2 caught this; fixed
     before commit.
  2. No --model flag. Per DOCS-4 full flag enumeration there is no
     --model flag in programmatic mode. Model selection happens via
     ~/.vibe/config.toml. The IR's `model` field is used by OLP for
     routing only; Vibe uses whatever model is in user-level config.
     Documented in lossy translations + as A5 CONFIRMED-NOT-APPLICABLE.
  3. Canonical IDs primary, short forms as aliases. models-registry.json
     uses devstral-2-25-12 / devstral-small-2-25-12 as primary `id`s
     matching the canonical Mistral models registry; user-facing short
     forms (devstral-2, devstral-small-2, devstral, devstral-small) are
     aliases. Plugin's models[] array includes both canonical IDs AND
     alias keys so getProviderForModel routes either form. Same pattern
     codified for Codex aliases in D6.
  4. Auth precedence: MISTRAL_API_KEY env > ~/.vibe/.env > null.
     Documented in DOCS-2. readAuthArtifact supports
     MISTRAL_VIBE_AUTH_PATH env override for testing.
  5. Mistral stays Candidate. STATIC_REGISTRY.length === 3 but
     loadProviders({}) returns empty Map; only loadProviders({
     enabled: { mistral: true }}) loads it. POST /v1/chat/completions
     devstral-* still returns 503 until config flag is set and E2E
     audit passes.

Reviewer chain (Iron Rule 10):
  Implementer: sonnet (general-purpose).
  Fresh-context reviewer: opus (ecc:code-reviewer). Verdict round-1:
    REQUEST_CHANGES (2 blockers caught).
  Reviewer ran npm test (222/222 pass), WebFetched all 8 canonical
    Mistral docs URLs, discovered DOCS-8 (models registry) which
    sonnet missed — exactly the D6 failure pattern, repeated. Reviewer
    independently verified `--output streaming` vs `json` semantics
    against the live docs page text, not paraphrases.

Reviewer blocking findings folded in this commit:
  B-1 (--output json wrong): Plugin now passes `--output streaming` per
    docs verbatim. Test "irToMistral: user message → args with --prompt
    and --output streaming" updated to assert the new arg and reject
    the old one.
  B-2 (canonical models page missed): models-registry.json refactored
    to use date-stamped canonical IDs as primary; short forms as
    aliases. mistral.mjs header adds DOCS-8 as the new canonical
    authority pin for model IDs. Plugin's models[] array merges
    canonical + alias keys so existing routing tests pass with either
    form.
  B-3 (404 claim incorrect): mistral.mjs header DOCS-MAIN updated.
    docs.mistral.ai/mistral-vibe/overview is 200 OK; sonnet's 404
    claim was a path-normalization mismatch.

Reviewer non-blocking suggestions (deferred to D-later / not D8 scope):
  - VIBE_HOME env override has no Suite 12 test (implementation
    present at mistral.mjs:262). Parallel gap to D6 CODEX_HOME.
  - _extractKeyFromDotenv has no direct unit test.
  - config.toml model selection mechanism (Vibe-specific quirk —
    no --model flag means OLP can't pass model per request). A D-later
    ADR note will discuss whether OLP should write a project-local
    ./.vibe/config.toml before spawn or accept the user-level config
    as authoritative.

Test count: 174 (after D6) → 222 (after D8).

Verification:
  node --check on all touched files: clean.
  npm test on Node 25.8.0: 222/222 pass in 210ms.
  STATIC_REGISTRY = [anthropic, openai, mistral] verified.
  hygiene grep: zero personal-name/path/token hits. Fixtures use
    <fake-mistral-api-key> placeholders.

Co-Authored-By: Claude Opus 4.7 (noreply@anthropic.com)
2026-05-23 21:48:29 +10:00

116 lines
4.2 KiB
JavaScript

/**
* lib/providers/index.mjs — Static provider registry
*
* Authority: ADR 0002 § "Loading model"
*
* This file is a hand-maintained static enumeration. There is no filesystem
* scan, no dynamic discovery, no npm-installed plugin loading. Adding a
* provider requires: (1) write lib/providers/<name>.mjs, (2) add one import +
* one entry to STATIC_REGISTRY below, (3) README § "Supported Providers",
* (4) inclusion ADR per ADR 0006.
*
* At D4 (Phase 1 Day 2), the anthropic plugin is added to STATIC_REGISTRY
* as a Candidate. It is NOT Enabled by default — a config with
* { enabled: { anthropic: true } } is required, which only happens after D5
* E2E audit passes per ALIGNMENT.md § Provider Inventory.
*
* At D6 (Phase 1 Day 4), the openai (Codex) plugin is added to STATIC_REGISTRY
* as a Candidate. Enabled by { enabled: { openai: true } } after D7 E2E audit.
*
* At D8 (Phase 1 Day 5), the mistral (Mistral Vibe) plugin is added to
* STATIC_REGISTRY as a Candidate. Enabled by { enabled: { mistral: true } }
* after D-later E2E audit. Authority: ADR 0006 Tier D classification for
* Mistral Vibe (Le Chat Pro); docs-based authority from
* https://docs.mistral.ai/mistral-vibe/terminal/quickstart.
*/
import { validateProvider } from './base.mjs';
import anthropicDefault from './anthropic.mjs';
import codexDefault from './codex.mjs';
import mistralDefault from './mistral.mjs';
// Normalize default export pattern
const anthropic = anthropicDefault;
const codex = codexDefault;
const mistral = mistralDefault;
// ── Static registry ───────────────────────────────────────────────────────
const STATIC_REGISTRY = [
anthropic,
codex,
mistral,
];
// ── Registry functions ────────────────────────────────────────────────────
/**
* Loads and validates providers, filtering by the enabled set in config.
* At D4, the anthropic provider is Candidate only — it will not appear in
* the loaded Map unless config.enabled.anthropic === true (set by D5 after E2E).
*
* @param {{ enabled?: Record<string,boolean> }} [config={}]
* @returns {Map<string, import('./base.mjs').ProviderContractV1>}
*/
export function loadProviders(config = {}) {
const loaded = new Map();
for (const p of STATIC_REGISTRY) {
const { valid, errors } = validateProvider(p);
if (!valid) {
throw new Error(`Provider ${p?.name ?? 'unknown'} fails contract validation: ${errors.join('; ')}`);
}
const enabled = config.enabled?.[p.name] === true;
if (enabled) {
loaded.set(p.name, p);
}
}
return loaded;
}
/**
* Returns the first provider in `loadedProviders` whose `models[]` includes
* the exact `modelString`. Returns null if no provider is found.
*
* This is the naive lookup strategy for D4. Phase 2+ may add prefix/alias
* matching once models-registry.json is consulted per AGENTS.md SPOT policy.
*
* @param {Map<string, import('./base.mjs').ProviderContractV1>} loadedProviders
* @param {string} modelString
* @returns {{ provider: import('./base.mjs').ProviderContractV1, name: string }|null}
*/
export function getProviderForModel(loadedProviders, modelString) {
for (const [name, p] of loadedProviders) {
if (p.models.includes(modelString)) {
return { provider: p, name };
}
}
return null;
}
/**
* Looks up a provider by name in the loaded Map. Returns null if not found.
* Useful for tests that need to access a specific provider directly.
*
* @param {Map<string, import('./base.mjs').ProviderContractV1>} loadedProviders
* @param {string} name
* @returns {import('./base.mjs').ProviderContractV1|null}
*/
export function getProviderByName(loadedProviders, name) {
return loadedProviders.get(name) ?? null;
}
/**
* Returns all provider names in the static registry (whether enabled or not).
* Used by /health and diagnostics.
* At D4: returns ['anthropic']; at D6: returns ['anthropic', 'openai'];
* at D8: returns ['anthropic', 'openai', 'mistral'].
*
* @returns {string[]}
*/
export function listAllProviderNames() {
return STATIC_REGISTRY.map(p => p.name);
}