Skip to content

vibe provider: native wire-protocol emulation of the Mistral Vibe binary (no ACP host, no subprocess) #1653

Closed

Description

@developing-today

Design correction (2026-11-04, Drew): the original ACP-client-host design on this issue is withdrawn. Drew: "the other direct providers do not use acp they emulate exactly how the binary works." This issue now specifies a native wire-protocol provider, like antigravity and mistral in packages/ai. No vibe subprocess, no ACP host, no vibe-acp.

What to build

A provider package @opencode/ai/providers/vibe (provider id vibe) in this repo:

  • packages/ai/src/providers/vibe.ts — the package (mirrors providers/mistral.ts: same MistralChat protocol route, same endpoint), registered in providers/index.ts and in the builtins map in packages/core/src/provider.ts.
  • packages/ai/src/providers/vibe-auth.ts — the credential resolver (mirrors providers/antigravity-auth.ts: secret-tool lookup, in-memory cache + invalidate(), MissingCredentialError when absent).
  • Users select it like the other direct providers: declare provider vibe in opencode config with npm: "@opencode/ai/providers/vibe" and the models below (models.dev carries no vibe entry; same contract as antigravity).

Wire spec — verified from the installed mistral-vibe 3.1.0 source on amd-6

Package: ~/.local/share/uv/tools/mistral-vibe/lib/python3.12/site-packages/vibe/ (+ mistralai_vibe_local_harness/).

Completion request (the only call the provider makes)

  • POST https://api.mistral.ai/v1/chat/completions, streamed (SSE), via the official mistralai SDK: Mistral(api_key, server_url = api_base minus any /vN suffix); vibe's preset api_base is https://api.mistral.ai/v1 — identical to this repo's MistralChat.DEFAULT_BASE_URL, so the existing MistralChat protocol route is reused unchanged.
  • Headers: Authorization: Bearer <key> is the ONLY auth header on the API-key path. No tenant/workspace/org headers exist anywhere in the binary. Provider extra_headers default empty; x-affinity: <id> appears only when a session affinity id exists (not applicable here).
  • Body: model (route model id), messages, temperature (top_p omitted when temperature is set — Mistral rejects both), reasoning_effort when the model reasons (off is sent only to known-reasoning models), max_tokens when capped, tools + parallel_tool_calls: true when tools are present. Usage comes from the stream's usage object (prompt/completion/total + cached input tokens) — the route's existing usage mapping covers this.

Models (Vibe's own presets)

Default: mistral-vibe-cli-latest (alias mistral-medium-3.5, "Mistral Medium 3.5"), temperature 1.0, thinking high (levels off/high), context 262144, image input, cost in/out/cache-read 1.5 / 7.5 / 0.15 per M tokens.
Selectable: mistral-medium-latest, mistral-large-latest, mistral-small-latest, codestral-latest, ministral-14b-latest, ministral-8b-latest, ministral-3b-latest. (Vibe's third-party GLM presets ride a different provider preset and stay out of scope; the zai provider covers them.)

Credential resolution — mirror the binary exactly

The binary's resolve_api_key_with_origin order (first hit wins):

  1. Explicit provider config (apiKey / custom auth) — fork-level override for tests and offline use, mirroring antigravity's Config.
  2. Environment MISTRAL_API_KEY — the binary's process env already includes ~/.vibe/.env values (loaded under the real env; real env wins), so the resolver checks env MISTRAL_API_KEY, then the ~/.vibe/.env dotenv file itself (0600; the binary writes it only when no keyring backend exists).
  3. OS keyring: secret-tool lookup service ai.mistral.vibe username MISTRAL_API_KEY; legacy service vibe as fallback (the binary migrates legacy entries forward). The browser sign-in (vibe --setup, PKCE against console.mistral.ai) stores a plain workspace API key there — sent as Bearer verbatim; no refresh, no exchange (unlike antigravity's OAuth JSON).

Keyring read mechanism (must work from this repo's TS runtime on amd-6): exec secret-tool located via SECRET_TOOL_PATH -> PATH -> /nix/store scan for libsecret (the antigravity-auth pattern). Python keyring is broken on amd-6 (kwalletd6 owns org.freedesktop.secrets and its Secret Service cannot do the DH session python-secretstorage mandates; gnome-keyring runs pkcs11-only) — do not use it. DBUS_SESSION_BUS_ADDRESS defaults to unix:path=/run/user/<uid>/bus when unset; 15s timeout; any failure = credential absent, never a hang.

Out of scope (the binary's first-party side calls, not the provider's)

  • Model catalog GET {api_base}/model-catalog?surface=vibe-code (fail-open, cached) and GET https://console.mistral.ai/api/vibe/whoami — the provider ships Vibe's preset table in code instead.
  • The experimental OIDC sign-in (VIBE_CLI_OIDC_ENABLED=1, issuer auth.mistral.ai, keyring bundle under service ai.mistral.vibe.oidc) — the default browser sign-in provisions an API key; OIDC is not the shipped flow.

Acceptance

  • Unit tests: resolution order (config > env > .env file > keyring primary > keyring legacy; absent everywhere -> the not-signed-in error) and request shaping (endpoint, Bearer-only auth, default model carries temperature 1.0 + reasoning_effort: "high", caller-supplied values win).
  • Live: after the one-time desktop sign-in lands the keyring credential, an opencode session using provider vibe completes a real task end-to-end with no vibe process involved.
  • Not signed in: any vibe request fails fast with a clear error naming the missing credential and the fix (run vibe --setup once, or set MISTRAL_API_KEY).

Acceptance criteria

  • Provider package @opencode/ai/providers/vibe (id vibe) exists in packages/ai/src/providers/vibe.ts, mirroring the MistralChat protocol route and endpoint — no vibe subprocess, no ACP host.
  • vibe-auth.ts resolves credentials like the other direct providers (secret-tool lookup, in-memory cache + invalidate(), MissingCredentialError when absent).
  • Provider is registered in providers/index.ts and the builtins map in packages/core/src/provider.ts, and is selectable via config as specified in the body.
  • PR feat(ai): vibe provider — native emulation of the Mistral Vibe binary (#1653) #1661 (already open, implementing this issue) is shepherded to merge or superseded by an equivalent PR that closes this issue.

Activity

  1. changed the title [-]vibe provider: host vibe-acp (Mistral Vibe) as an ACP-client provider seat[/-] [+]vibe provider: native wire-protocol emulation of the Mistral Vibe binary (no ACP host, no subprocess)[/+] on Nov 4, 2026
  2. dezren39 commented on Nov 4, 2026

    @dezren39

    🤖 Feeder triage · issue-sweep-rotating (Make triage feeder) · 2026-11-04

    Checked (fresh, verified twice): current main ea2202ef5a65f3839a48c6b3e87918f4b7a7720a. The gap this issue specifies is still present on main:

    • packages/ai/src/providers/ (via gh api contents listing) contains mistral.ts, antigravity.ts, and antigravity-auth.ts — the two mirror patterns this issue names — but no vibe.ts and no vibe-auth.ts.
    • packages/ai/src/providers/index.ts exports Antigravity (line 4) and Mistral (line 33); there is no Vibe export.
    • The issue carries no labels and no comments, and no live task for it exists in the repo-health-opencode-pilot queue (queued/claimed/parked lists checked) — it has not been picked up yet.

    Verdict: CONFIRMED — not started. The spec is complete and self-contained (wire spec verified from the installed mistral-vibe 3.1.0 source, credential resolution order, model presets, acceptance tests).

    What "done" looks like: the issue's own Acceptance section — vibe + vibe-auth providers mirroring mistral/antigravity-auth, registered in providers/index.ts and the builtins map in packages/core/src/provider.ts; unit tests for resolution order (config > env > ~/.vibe/.env > keyring primary > keyring legacy; absent everywhere → the not-signed-in error) and request shaping (Bearer-only auth, default model temperature 1.0 + reasoning_effort: "high"); a live end-to-end session once the one-time desktop sign-in lands the keyring credential. No vibe subprocess, no ACP host.

    Enqueuing as a triage packet on repo-health-opencode-pilot from this feeder run.

  3. dezren39 commented on Nov 4, 2026

    @dezren39

    agent-note:v1
    🤖 Agent note · state: queued (task 9709)

  4. added
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
  5. dezren39 commented on Nov 4, 2026

    @dezren39

    Triage verdict — task 9709, lane opencode-a, 2026-11-04 (fresh main ac0ada4)

    CONFIRMED not-started; implementation shape confirmed against current main; fix packet enqueued as task 9711.

    • Gap verified: packages/ai/src/providers/ has no vibe.ts / vibe-auth.ts; packages/ai/src/providers/index.ts exports Antigravity (:4) and Mistral (:33) with no Vibe export; the builtins map in packages/core/src/provider.ts registers @opencode/ai/providers/antigravity (:70) and .../mistral (:113) with no vibe entry.
    • Shape confirmed: new vibe.ts + vibe-auth.ts, exported from providers/index.ts, registered in the core builtins map; no vibe subprocess, no ACP host. The secret-tool pattern to mirror already exists in antigravity-auth.ts (:88-99 — PATH scan, /nix/store candidates, SECRET_TOOL_PATH override).
    • Unit-test plan (in task 9711's packet): credential resolution order (config > env MISTRAL_API_KEY > ~/.vibe/.env > keyring ai.mistral.vibe > keyring legacy vibe; absent everywhere → not-signed-in error naming vibe --setup / MISTRAL_API_KEY) and request shaping (endpoint, Bearer-only auth, default model + temperature 1.0 / reasoning_effort high, caller values win), via the packages/ai test suite.
    • Live acceptance deferred correctly: the end-to-end leg needs the one-time desktop vibe --setup sign-in landing the keyring credential. Triage could NOT verify keyring presence from the sandbox shell (secret-tool is not on PATH there — lookup exit 127; where runnable, the legacy service returned absent), so the live leg stays pending and the implementation packet explicitly does not block on it.

    agent-note:v1 — repo-queue-worker-shift-opencode lane A, task 9709 triaged, fix = task 9711

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Nov 4, 2026
  7. dezren39 commented on Nov 4, 2026

    @dezren39

    🤖 Agent note · bound

  8. added
    user/reviewNeeds a user-look: Muse reviews first and handles what it can
    on Nov 4, 2026
  9. 9 remaining items

  10. added 2 commits that reference this issue on Nov 5, 2026
  11. added
    work/assignedAn agent holds this item via the queue (claimed task)
    queue/claimedA queue worker has claimed the task for this item
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  12. dezren39 commented on Nov 5, 2026

    @dezren39

    Task 9711 picked up by repo-worker-opencode on lane 3fe37e8a-c61b-419c-9e97-769ecd264d5d. Fresh GitHub state revealed PR #1661 was already bound to this task and has merged. I verified its merged diff includes the provider, credential resolution, provider registration, tests, and the Vibe attribution metadata correction. No code or PR changes were made by this lane.

  13. dezren39 commented on Nov 5, 2026

    @dezren39

    Closing after PR #1661 merged at 2026-11-05T00:30:03Z. Fresh review verified merged commit 7774bf0 includes the native Vibe provider and auth, registration, request-shaping and credential tests, and the follow-up metadata.call_source stamp plus caller-override coverage. The PR commit reports 1932 packages/ai tests passing and clean typecheck; no CI checks were reported by gh pr checks.

  14. removed
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Nov 5, 2026
  15. developing-today commented on Nov 5, 2026

    @developing-today
    Author

    Live acceptance for the provider spec'd here: PASS against PR #1661 — full proof in #1661 (comment)

    An opencode session (standalone, PR-branch build) completed a real task through provider vibe / mistral-vibe-cli-latest with no interactive step, authenticated solely by the keyring credential from the one-time vibe --setup sign-in (env carried no MISTRAL_API_KEY). The two failures on the way were acceptance-harness defects (Bun CWD tsconfig discovery; a context-only model limit dropped at config normalization), documented in the linked comment — no provider changes were needed.

  16. dezren39 commented on Nov 6, 2026

    @dezren39

    Root cause found + fixed (2026-11-06): vibe-direct was spending the wrong KEY

    vibe-direct's wire emulation was faithful — endpoint, stamp, presets all as designed. The failure was credential resolution: Mistral meters per key, not by call metadata, and the server environment handed vibe-direct the API-budget key.

    Evidence

    • Two distinct 45-char keys exist on the laptop: the keyring ai.mistral.vibe sign-in key (sha256 fp b782943c6ef9f380…) and the server env MISTRAL_API_KEY (fp 144cdb68d8d89c4b…) — the latter is byte-identical to the enrolled mistral integration credential (the account API key).
    • Control: the real vibe CLI (3.1.0) on the keyring key returns PONG while the API budget is exhausted. The same CLI forced onto the env key returns the exact production error: 402 {"message":"API budget exhausted.","type":"billing_api_budget_exhausted","code":"2303"}.
    • Four-cell curl matrix (key × stamp): keyring key → 200 with or without the vibe metadata; env key → 402 even WITH the stamp. The call_source: "vibe_code" stamp neither moves billing nor is required for plan billing. (The header comment in packages/ai/src/providers/vibe.ts claiming the stamp is "the metering key" was wrong — corrected in docs(vibe): billing follows the key, not the metadata stamp #1808.)
    • vibe-auth.ts faithfully mirrors the binary: env → ~/.vibe/.env → keyring. The trap was upstream: the code repo's home-manager config materialized MISTRAL_API_KEY (from sops, the Bitwarden mistral.api-key) into the opencode server unit's EnvironmentFile, so tier-1 env resolution always won and the keyring plan key was never reached.

    Attribution for the 2026-11-05 API burn

    Yes — our own traffic. Route history: 8,693 vibe-direct attempts on 2026-11-05, 8,660 of them inside the window in which the API pool went 15.986% → 100% (walled 22:45Z), while vibe-direct carried the deepseek-flash-plan load. The pool burn was the mis-keyed vibe leg, not external API use.

    Fix

    • Runtime (live 2026-11-06 07:19Z): user drop-in opencode.service.d/10-vibe-keyring-fix.conf swaps the unit's ExecStartPre for a scrub that deletes only the MISTRAL_API_KEY= line from the materialized env file (DEEPSEEK line preserved). Verified: server env carries no MISTRAL_API_KEY; vibe-direct/mistral-small-latest → OK and vibe-direct/mistral-vibe-cli-latest → OK with the API pool still walled; mistral-plan/mistral-small-latest → served by the vibe leg (after the stale pre-fix quota cooldown/skip adjustments from the mis-keyed era expired/were cleared).
    • Durable: fix(opencode): stop materializing MISTRAL_API_KEY into the server env (vibe-direct must bill the Vibe plan) developing-today/code#6460 (merged, 01c97fe2) — the Nix materializer no longer writes MISTRAL_API_KEY; config comments corrected. Lands on the next switch; the drop-in + scrub script must be removed at that point (noted in the PR).
    • This repo: docs(vibe): billing follows the key, not the metadata stamp #1808 (merged, 8cf16377) — vibe.ts header comment corrected.

    The mistral provider leg is unaffected by the env removal: its enrolled credential is its source (verified live: it returns the budget 402, not an auth error, with the env var absent).

    One residual, by design: the router's attempt-reported quota adjustments written during the mis-keyed era (30-min skip on both vibe-direct/ and mistral/, refreshed by any 402 drawn during restarts) can briefly re-wall the route after a restart until they expire; they do not renew now that the vibe leg succeeds.

  17. dezren39 commented on Nov 6, 2026

    @dezren39

    Verification timeline correction + one residual quirk (2026-11-06, later)

    Two corrections to the comment above, for the record:

    1. Route recovery landed at ~07:50Z, not 07:19Z. The direct vibe-direct leg verified immediately after the env fix (attempt successes 07:19:46Z and 07:20:34Z), but the mistral-plan route stayed walled until ~07:50Z by attempt-reported quota adjustments written during the mis-keyed era and refreshed by two restart-transition 402s at 07:19:16Z (30-min skips on vibe-direct/ and mistral/, route_note 5685/5686), plus the stale 05:15Z quota cooldown (cleared by cooldown-end rows in route_health). Since 07:50:08Z the route has been drawn repeatedly in live fleet traffic (route speed → deepseek-flash-plan → mistral-plan), choosing vibe-direct every time with successful attempts; the mistral leg is excluded by its own credential quota cooldown, which is correct — its budget is genuinely full. (An earlier progress note of mine reported a 07:50Z success prematurely via a corrupted background-tool result; the figures in this comment are re-read from route_decision/route_attempt directly.)

    2. Residual quirk (independent of the credential bug): a bare opencode run -m mistral-plan/mistral-small-latest (with or without a #variant) fails instantly with SessionRunnerModel.ModelUnavailableError and writes no route_decision — the model never reaches route resolution on that path. Route groups serve normally when drawn through route-context sessions (the claimant/fleet path, and as members of other routes). Observed consistently before and after every credential gate cleared, so it is not the budget bug; flagging it here in case a human ever pins a route group directly from the CLI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/mMedium: expected under 1 day of workkind/enhancementNew feature or requestmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedmeta/protocol-gapAn agent touched this without the required agent-note comment or labels; needs review of whypriority/normalOrdinary priority (the default)source/dispatchFiled by pm-dispatchuser/reviewNeeds a user-look: Muse reviews first and handles what it can

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions