opencode-provider-switch/.trellis/tasks/archive/2026-04/04-17-04-17-ops-mvp-design-review/prd.md

16 KiB

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/<alias> 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

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:

{
  "$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/<alias> 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/<alias>.

Example

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.

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.<id>.
  2. Custom model IDs come from keys under provider.<id>.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/<alias> 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

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