d67d30c58a
CI / build (push) Successful in 1m57s
Points an opencode worker at a local vLLM (or llama.cpp / LM Studio / TGI) instead of opencode's own gateway. opencode has no ANTHROPIC_BASE_URL seam, so this could not be a config-only change: setting baseUrl on a kind: opencode profile now makes the launcher emit a custom `provider` block into the generated opencode.json, using @ai-sdk/openai-compatible. The provider id comes from the provider half of the model: selector, so one field drives both the generated declaration and the -m flag and the two cannot drift apart. A bare model name with a baseUrl set is rejected at spawn with a message saying how to fix it — silently falling back to the default gateway would leave a worker talking to the wrong LLM while looking perfectly healthy. A bare host:port gets /v1 appended (where these servers mount the API); a URL that already carries a path is used verbatim. tokenEnv, when set, becomes the provider apiKey; local servers generally ignore it but the AI SDK requires a non-empty value, so a placeholder is used otherwise. Two supporting changes: - writeConfig previously ran only when a bridge MCP url was set. A pinned endpoint needs the config file too, so it now runs when either applies, and the mcp/instructions half is emitted conditionally. - The config is now built with Jackson instead of string concatenation. The provider block is nested and interpolates operator-supplied values (URL, model id, api key), so escaping has to be real rather than a hand-rolled two-character replace. No guard entry is required even with baseUrl set. SubscriptionGuard exists to stop a worker borrowing the primary's Anthropic subscription, and an opencode process has no Anthropic credential path at all — the asymmetry with the Claude adapter reusing the same field is deliberate and documented at the call site. Also fixes a brittle assertion in the existing MCP-mount test, which matched the substring "\"type\": \"remote\"" and broke on Jackson's spacing. It now parses the generated JSON and asserts on structure; whitespace is the formatter's business, not the contract's. 318 tests (was 311): 5 new covering provider generation, /v1 normalisation, path-preserving URLs, the missing-prefix rejection, MCP+provider coexistence, and that no baseUrl still means no provider block. Verified live end to end: daemon restarted on this build, worker spawned on the opencode-local profile, generated config carries baseURL http://127.0.0.1:8000/v1, and a blocking bridge_send returned {"reply":"LOCAL-OK","replySource":"reply"} — a structured reply, not the completion fallback. The worker pane reports "Build · deepseek-v4-flash local-vllm (bridged)", confirming traffic reached the local server rather than silently falling back.