docs(trellis): finalize OPS MVP review PRD
Lock the reduced MVP around alias-driven OpenAI Responses failover so follow-up implementation starts from a stable, source-verified contract.
This commit is contained in:
parent
b796a61469
commit
332fa33637
@ -0,0 +1,2 @@
|
||||
{"file": ".opencode/commands/trellis/finish-work.md", "reason": "Finish work checklist"}
|
||||
{"file": ".opencode/commands/trellis/check.md", "reason": "Code quality check spec"}
|
||||
@ -0,0 +1 @@
|
||||
{"file": ".opencode/commands/trellis/check.md", "reason": "Code quality check spec"}
|
||||
@ -0,0 +1 @@
|
||||
{"file": ".trellis/workflow.md", "reason": "Project workflow and conventions"}
|
||||
493
.trellis/tasks/04-17-04-17-ops-mvp-design-review/prd.md
Normal file
493
.trellis/tasks/04-17-04-17-ops-mvp-design-review/prd.md
Normal file
@ -0,0 +1,493 @@
|
||||
# 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
|
||||
|
||||
```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/<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
|
||||
|
||||
```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.<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
|
||||
|
||||
## 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
|
||||
64
.trellis/tasks/04-17-04-17-ops-mvp-design-review/task.json
Normal file
64
.trellis/tasks/04-17-04-17-ops-mvp-design-review/task.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"id": "04-17-ops-mvp-design-review",
|
||||
"name": "04-17-ops-mvp-design-review",
|
||||
"title": "PRD Review: OPS MVP Design",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": "docs",
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "Apale",
|
||||
"assignee": "Apale",
|
||||
"createdAt": "2026-04-17",
|
||||
"completedAt": "2026-04-17",
|
||||
"branch": null,
|
||||
"base_branch": "master",
|
||||
"worktree_path": null,
|
||||
"current_phase": 0,
|
||||
"next_action": [
|
||||
{
|
||||
"phase": 1,
|
||||
"action": "brainstorm"
|
||||
},
|
||||
{
|
||||
"phase": 2,
|
||||
"action": "research"
|
||||
},
|
||||
{
|
||||
"phase": 3,
|
||||
"action": "implement"
|
||||
},
|
||||
{
|
||||
"phase": 4,
|
||||
"action": "check"
|
||||
},
|
||||
{
|
||||
"phase": 5,
|
||||
"action": "update-spec"
|
||||
},
|
||||
{
|
||||
"phase": 6,
|
||||
"action": "record-session"
|
||||
}
|
||||
],
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
".trellis/tasks/04-17-04-17-ops-mvp-design-review/prd.md",
|
||||
"/home/apale/Code/opencode/packages/opencode/src/provider/provider.ts",
|
||||
"/home/apale/Code/opencode/packages/opencode/src/cli/cmd/models.ts",
|
||||
"/home/apale/Code/opencode/packages/opencode/src/config/config.ts",
|
||||
"/home/apale/Code/opencode/packages/opencode/src/auth/index.ts"
|
||||
],
|
||||
"notes": "Review task completed. PRD was redesigned around alias-driven OpenAI Responses failover, with MVP decisions locked from direct OpenCode source analysis. Implementation will proceed in a follow-up task.",
|
||||
"meta": {
|
||||
"mvpProtocol": "openai-responses",
|
||||
"syncDefaultScope": "global-opencode-config",
|
||||
"importBoundary": "config-defined-@ai-sdk/openai-only",
|
||||
"doctorMode": "static-default"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user