diff --git a/.trellis/tasks/04-17-04-17-ops-mvp-design-review/check.jsonl b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/check.jsonl new file mode 100644 index 0000000..310b03c --- /dev/null +++ b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/check.jsonl @@ -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"} diff --git a/.trellis/tasks/04-17-04-17-ops-mvp-design-review/debug.jsonl b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/debug.jsonl new file mode 100644 index 0000000..5b204df --- /dev/null +++ b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/debug.jsonl @@ -0,0 +1 @@ +{"file": ".opencode/commands/trellis/check.md", "reason": "Code quality check spec"} diff --git a/.trellis/tasks/04-17-04-17-ops-mvp-design-review/implement.jsonl b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/implement.jsonl new file mode 100644 index 0000000..4752e3d --- /dev/null +++ b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/implement.jsonl @@ -0,0 +1 @@ +{"file": ".trellis/workflow.md", "reason": "Project workflow and conventions"} diff --git a/.trellis/tasks/04-17-04-17-ops-mvp-design-review/prd.md b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/prd.md new file mode 100644 index 0000000..5340277 --- /dev/null +++ b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/prd.md @@ -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/` 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 diff --git a/.trellis/tasks/04-17-04-17-ops-mvp-design-review/task.json b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/task.json new file mode 100644 index 0000000..e8ebf47 --- /dev/null +++ b/.trellis/tasks/04-17-04-17-ops-mvp-design-review/task.json @@ -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" + } +}