# OPS MVP Redesign ## Summary `opencode-provider-switch` (`ops`) is a local proxy for OpenCode focused on one narrow job: - expose one stable local provider to OpenCode - let users select logical model aliases instead of concrete upstream models - route one alias to multiple upstream providers/models in fixed priority order - automatically fail over when upstream providers are temporarily unavailable This redesign intentionally removes most of the previous PRD scope. MVP is no longer about full OpenCode config takeover, protocol pools, provider discovery, or broad migration automation. MVP is about one thing: **multi-provider failover behind a stable OpenCode model alias**. ## Product Goal When a user chooses `ops/` inside OpenCode, `ops` should transparently try the configured upstream targets in priority order until one succeeds, without the user needing to care which provider actually served the request. ## Core User Need User already has, or can define, multiple OpenCode-compatible upstream providers. User wants: 1. one stable model name inside OpenCode 2. multiple upstream fallbacks behind that name 3. deterministic failover behavior 4. local proxy integration without switching away from OpenCode workflow ## Confirmed MVP Decisions - Protocol support: `openai-responses` only - Responses streaming: required in first shippable MVP - Local proxy shape: one OpenAI Responses-compatible local provider - OpenCode integration target: custom provider using `@ai-sdk/openai` - Alias support: required - Failover priority order: required and explicit - `auth.json` is not a provider-definition source for MVP import - Upstream model discovery from provider `/models`: out of MVP - Full OpenCode config install/restore takeover: out of MVP - Billing/cost accounting: out of MVP - Multi-protocol routing: out of MVP - Anthropic native support: out of MVP - Dashboard/web UI: out of MVP ## Non-Goals 1. No attempt to become a general AI gateway. 2. No automatic routing by latency, prompt type, or cost. 3. No broad migration of every OpenCode provider shape. 4. No provider capability normalization across vendors. 5. No background health scoring system. 6. No project-wide interception guarantees across every OpenCode config layer. 7. No mid-stream failover or stream splicing across upstream providers. ## Architecture in One Sentence OpenCode sends `POST /v1/responses` to local provider `ops`; `ops` resolves requested alias to an ordered target list and proxies request to first healthy upstream candidate. ## High-Level Architecture ```text OpenCode -> custom provider `ops` (@ai-sdk/openai) -> http://127.0.0.1:9982/v1/responses -> alias resolver -> failover engine -> upstream provider/model #1 -> upstream provider/model #2 -> upstream provider/model #3 ``` ## Integration Strategy With OpenCode ### Local Provider Shape MVP should expose exactly one local provider to OpenCode: - provider id: `ops` - npm package: `@ai-sdk/openai` - base URL: `http://127.0.0.1:9982/v1` - local API key: static placeholder such as `ops-local` Conceptual OpenCode config shape: ```jsonc { "$schema": "https://opencode.ai/config.json", "provider": { "ops": { "npm": "@ai-sdk/openai", "name": "OPS", "options": { "baseURL": "http://127.0.0.1:9982/v1", "apiKey": "ops-local" }, "models": { "gpt-5.4": { "name": "gpt-5.4" }, "gpt-5.4-mini": { "name": "gpt-5.4-mini" } } } }, "model": "ops/gpt-5.4" } ``` ### Why This Is Enough For `/models` OpenCode source confirms this path is valid. `opencode models` calls `Provider.list()` and prints entries from each resolved `provider.models` map. OpenCode Web/TUI model pickers also read runtime provider state and only surface models from connected providers. OpenCode provider loading also merges custom config-defined providers and models into runtime provider state. That means `ops` does **not** need a special external model catalog protocol for MVP. Simplest MVP path: - keep alias list in `ops` - sync alias list into OpenCode `provider.ops.models` - make sure `provider.ops` is valid enough to appear as a connected runtime provider - let OpenCode surface `ops/` in `/models` and `/model` ### Important Scope Rule MVP should **not** rewrite the entire OpenCode config. Instead, `ops` should only support a narrow integration step: - ensure `provider.ops` exists or is updated - optionally sync alias entries into `provider.ops.models` - optionally let user set `model` or `small_model` manually This avoids the previous PRD's high-risk install/restore workflow. ### Config Precedence Caveat OpenCode source also confirms config is merged from multiple layers, and lower-precedence files can be overridden by project-local config. Implication for MVP: - `ops opencode sync` must know which config file it is targeting - MVP should not silently write one low-precedence file and assume aliases will appear at runtime - default target should be global user config under `~/.config/opencode/` - if global config already exists, reuse existing main file in this order: `opencode.jsonc`, `opencode.json`, `config.json` - if no global config file exists, create `~/.config/opencode/opencode.jsonc` - project `opencode.json`, `.opencode/`, `OPENCODE_CONFIG_DIR`, managed config, and account config are out of default sync scope unless user explicitly targets them ## Provider Source Model `ops` needs upstream provider definitions, but this is no longer the product center. MVP should support two input paths: 1. manual provider entry through `ops` CLI 2. one-shot import from one explicit OpenCode config file, defaulting to global user config ### Supported Provider Shape In MVP MVP should only promise import for OpenCode custom providers that already declare: - `npm: @ai-sdk/openai` - `options.baseURL` - `options.apiKey` or equivalent proxyable auth/header settings This keeps import logic narrow and predictable. Explicit MVP boundary: - import support is limited to config-defined `@ai-sdk/openai` custom providers - `@ai-sdk/openai-compatible` provider import is out of MVP even if source config contains it OpenCode source also shows that `auth.json` provides credentials for an existing provider ID, but does not by itself define a brand-new custom provider shape. A custom provider still needs config describing its `npm`, endpoint, and models. Implication for MVP: - `ops` should import provider definitions from config, not from merged runtime auth state - `auth.json` support, if ever added later, should be treated as credential enrichment for already-known provider IDs - MVP import does not need to evaluate account config, managed config, or remote well-known config layers ### Explicit Non-Goal For MVP MVP should not promise full support for every OpenCode credential source such as complex `/connect`-managed auth flows or provider-specific auth conventions. Those can be added later if needed. ## Alias Design Alias is the primary user-facing abstraction. User should use alias directly inside OpenCode. User should not need to know concrete upstream model IDs after alias is configured. ### Alias Rules 1. Every alias maps to one or more concrete targets. 2. Every alias must contain at least one enabled target. 3. Alias target order is explicit and defines failover priority. 4. Alias name must be unique within `ops`. 5. OpenCode should reference alias as `ops/`. ### Example ```text alias: gpt-5.4 1. provider: su8 model: gpt-5.4 2. provider: codexfm model: GPT-5.4 3. provider: relay-x model: gpt-5.4-2026-04-01 ``` ### Minimal Alias Metadata MVP alias record should contain: - `alias` - `display_name` optional - ordered target list - `enabled` Rich capability metadata such as `limit`, `attachment`, `reasoning`, `tool_call`, and `variants` should be treated as optional passthrough data, not required for MVP routing correctness. ## Proxy Behavior ### Supported Incoming Route - `POST /v1/responses` ### Request Flow 1. Receive request from OpenCode. 2. Read and buffer full JSON request body once. 3. Parse `model` from that JSON body. 4. Treat `model` as `ops` alias. 5. Resolve alias to ordered enabled targets. 6. Replace only alias model field with concrete upstream model ID. 7. Forward request to highest-priority target. 8. If failure is retryable and no response has started to client yet, replay same JSON body to next target. 9. Return first successful upstream response. ### Request Shape Constraint OpenCode source confirms real `@ai-sdk/openai` traffic uses OpenAI Responses API JSON payloads, typically with `stream: true`. Implication for MVP: - proxy only needs to support JSON `POST /v1/responses` requests emitted by OpenCode - image/file inputs that OpenCode already serializes into JSON data URLs remain in scope - multipart upload compatibility is out of MVP ### Streaming Rule Responses streaming is part of normal OpenCode usage, not an optional edge path. OpenCode source shows `@ai-sdk/openai` requests go to `/v1/responses` with `stream: true` by default. MVP proxy must therefore support streaming pass-through from first release. Failover rule for streaming: - failover allowed only before any response headers or body bytes are written to OpenCode - receiving an upstream response is not commitment by itself if proxy has not flushed anything downstream yet - once any headers or body bytes are written to client, current provider is locked for that request - if locked stream later fails, proxy returns failure from current upstream and does not switch mid-stream ### Why Mid-Stream Failover Is Out OpenCode source parses OpenAI Responses streams as stateful SSE event sequences, including text deltas, reasoning items, tool calls, and response lifecycle events. MVP should not attempt to splice or continue one started stream with another provider. ## Failover Policy MVP failover should be deterministic, sequential, and easy to reason about. ### Retryable Failures - DNS/connect failure - connection reset / EOF before first downstream byte - request timeout before first downstream byte - stream chunk timeout before first downstream byte - HTTP `429` before first downstream byte - HTTP `500-599` before first downstream byte ### Non-Retryable Failures - alias not found - alias has no enabled targets - HTTP `400` - HTTP `401` - HTTP `403` - HTTP `404` - upstream validation error after request accepted ### Why Conservative Rules Vendors return inconsistent error bodies. MVP should not try to infer hidden semantics such as "model not found, maybe retry elsewhere" from provider-specific error text. Conservative failover is preferable to surprising failover. ## Proxy Debugging Headers For debugging, `ops` should add response headers where possible: - `X-OPS-Alias` - `X-OPS-Provider` - `X-OPS-Remote-Model` - `X-OPS-Attempt` - `X-OPS-Failover-Count` These headers are cheap and make failover behavior understandable. ## Config and State Strategy Previous PRD centered around SQLite-first state. That is no longer necessary for MVP. MVP should prefer a simpler user-editable config shape unless implementation proves this too limiting. ### Recommended MVP Direction Use one local `ops` JSON or JSONC config file for: - upstream providers - aliases - local proxy settings Reason: - easiest to inspect and debug - lowest implementation cost - aligns with MVP goal of deterministic failover, not platform management SQLite is explicitly out of MVP. It can be reconsidered later if request logs, mutation history, or richer syncing become important. ## Minimal CLI Surface Recommended MVP commands: ### Core - `ops serve` - `ops doctor` ### `ops doctor` MVP Boundary First-release `ops doctor` should stay side-effect free. It should validate: - local `ops` config can be loaded - every alias resolves to at least one enabled target - local proxy bind address and config are internally consistent - generated or synced `provider.ops` config shape is structurally valid It should not, by default: - send real test requests to upstream providers - consume quota from user providers - mutate local or OpenCode config as part of diagnosis If live upstream probing is needed later, it should be added as explicit opt-in behavior such as `ops doctor --live`. ### Provider Management - `ops provider add` - `ops provider list` - `ops provider remove` - `ops provider import-opencode` ### Alias Management - `ops alias add` - `ops alias list` - `ops alias bind` - `ops alias unbind` - `ops alias remove` ### OpenCode Integration - `ops opencode sync` ## `ops opencode sync` Responsibility This command should do one narrow job: - by default update or create custom provider `ops` in global OpenCode user config - prefer existing global config file in this order: `opencode.jsonc`, `opencode.json`, `config.json` - if none exists, create `~/.config/opencode/opencode.jsonc` - sync current alias names into `provider.ops.models` Optional extra behavior: - set `model` only when user requests it explicitly, for example via a flag - set `small_model` only when user requests it explicitly, for example via a flag This command should not attempt a broad migration of existing providers. Default MVP behavior should be conservative: - do not rewrite existing OpenCode `model` - do not rewrite existing OpenCode `small_model` - do not write project-local OpenCode config unless user explicitly targets that file ## OpenCode `/models` Research Conclusion Research result from OpenCode source: 1. OpenCode custom providers are declared in config under `provider.`. 2. Custom model IDs come from keys under `provider..models`. 3. `opencode models` prints models from runtime `Provider.list()` state. 4. Web/TUI `/model` pickers read runtime provider lists and only show connected providers' models. 5. OpenCode supports alias key and real upstream model ID being different values. Conclusion: - alias exposure inside OpenCode is feasible in MVP - simplest path is config sync, not custom remote model registry work - syncing alias keys into `provider.ops.models` is sufficient for exposure if `provider.ops` lands in connected runtime provider state ## Security Model MVP security posture should stay simple: - listen on `127.0.0.1` by default - use static local placeholder API key between OpenCode and `ops` - store upstream credentials in local `ops` config - document that local credential storage is sensitive No multi-user or remote-network security guarantees in MVP. ## Success Criteria MVP is successful if a user can: 1. configure at least two upstream providers manually or through OpenCode sync 2. create an alias with ordered targets across those providers 3. run `ops opencode sync` and see alias names appear in OpenCode `opencode models` output and `/model` picker 4. select `ops/` in OpenCode without exposing concrete upstream model IDs 5. send normal streaming OpenAI Responses traffic through `ops` 6. get automatic failover when primary provider returns `429` or `5xx`, or fails before first downstream byte 7. observe that once a stream has started, later upstream failure is surfaced as failure rather than hidden mid-stream switching ## Recommended Implementation Order ### Phase 1 - bootstrap Go project - config file loader/writer - provider schema - alias schema ### Phase 2 - `ops opencode sync` - narrow OpenCode provider import - alias list exposure in OpenCode config - connected-provider validation in `ops doctor` ### Phase 3 - local proxy server - `/v1/responses` forwarding - JSON body replay for retry - streaming pass-through - sequential failover before first downstream byte ### Phase 4 - `ops doctor` - debugging headers and logs ## Finalized MVP Decisions The following implementation choices are now locked for first-release MVP: 1. `ops opencode sync` updates `provider.ops` and alias exposure only by default. It must not modify OpenCode `model` or `small_model` unless explicit opt-in flags are provided. 2. Provider import support is limited to config-defined `@ai-sdk/openai` custom providers. 3. `ops doctor` is static by default and must not issue live upstream requests unless future explicit opt-in behavior is added. ## Strong Recommendation Do not keep the old PRD as implementation basis. Old PRD optimized for a broader product: install/restore, protocol split, SQLite-first state, discovery, migration. New MVP should optimize for one thing only: - stable alias in OpenCode - multiple upstream targets behind it - deterministic local failover for OpenAI Responses traffic