diff --git a/.opencode/package.json b/.opencode/package.json index 4f6bdbe..d36ee0c 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@opencode-ai/plugin": "1.1.40" + "@opencode-ai/plugin": "1.4.3" } } \ No newline at end of file diff --git a/.trellis/tasks/04-17-ops-mvp-design/prd.md b/.trellis/tasks/04-17-ops-mvp-design/prd.md new file mode 100644 index 0000000..804afda --- /dev/null +++ b/.trellis/tasks/04-17-ops-mvp-design/prd.md @@ -0,0 +1,762 @@ +# OPS MVP Design + +## Summary + +`opencode-provider-switch` (`ops`) is a local CLI + proxy for OpenCode. + +Its job is narrow: + +- Accept OpenCode traffic on `http://127.0.0.1:9982` +- Route requests by protocol, not by provider brand +- Retry/fail over across unreliable upstream relay providers +- Let multiple upstream providers share one logical model alias +- Manage state in SQLite, not in an `ops` config file +- Rewrite OpenCode global config to point at the local proxy + +MVP intentionally does **not** try to become a general AI gateway, a dashboard product, or a provider-agnostic orchestration platform. + +## Confirmed Decisions + +- Language: Go +- Persistence: SQLite +- Default listen address: `127.0.0.1:9982` +- Delivery shape: CLI only +- MVP protocols: + - `openai-chat-completions` + - `openai-responses` +- Anthropic native protocol: out of MVP +- Logical model aliasing: required +- Billing/cost accounting: out of MVP +- Provider model discovery via `/models`: in MVP, best-effort + +## Product Goals + +1. Keep OpenCode usable when cheap relay providers fail intermittently. +2. Reduce OpenCode config complexity by centralizing provider management in `ops`. +3. Make failover behavior predictable, visible, and debuggable. +4. Preserve OpenCode-native workflow instead of asking users to switch tools. + +## Non-Goals + +1. No GUI, web dashboard, or hosted control plane. +2. No Anthropic native API support in MVP. +3. No automatic price tracking or billing reconciliation. +4. No smart routing based on prompt content, latency scoring, or cost optimization. +5. No distributed/high-availability deployment story. +6. No attempt to normalize every provider quirk in MVP. + +## Why Go + +Go is the right fit for this product shape: + +- `net/http` is strong enough for proxying and streaming without extra framework weight. +- Cross-platform static builds are straightforward. +- SQLite works well as a local state store. +- CLI ergonomics are mature with `cobra`. +- Concurrency model is a good fit for retries, request cancellation, and stream handling. + +## Core Design Principle + +`ops` should manage **protocol pools** and **logical aliases**, not raw provider selection inside OpenCode. + +OpenCode should see a small number of local proxy providers. `ops` should own: + +- upstream providers +- API keys and headers +- failover order +- alias to provider-model mapping +- model discovery cache + +This keeps OpenCode config stable even when upstream providers are added, removed, or reprioritized. + +## High-Level Architecture + +```text +OpenCode + -> local OpenAI-compatible provider config + -> ops proxy (127.0.0.1:9982) + -> protocol router + -> alias resolver + -> failover engine + -> upstream relay provider A / B / C +``` + +### Major Components + +1. `cli` + - all user-facing commands + - install, restore, provider management, alias management, doctor, serve + +2. `sqlite store` + - source of truth for providers, aliases, protocol pools, model cache, install state + +3. `proxy server` + - handles incoming HTTP requests from OpenCode + - supports streaming and non-streaming pass-through + +4. `router` + - resolves protocol + alias -> ordered upstream targets + +5. `failover engine` + - sequentially tries candidate targets based on priority and failure policy + +6. `opencode integration` + - imports current OpenCode config + - writes generated proxy-backed OpenCode config + - restores backup when requested + +7. `model discovery` + - fetches raw `/models` data from providers + - stores normalized cache for alias creation and diagnostics + +## Supported Protocols in MVP + +### 1. OpenAI Chat Completions + +- Incoming route: `POST /v1/chat/completions` +- Discovery route: `GET /v1/models` +- OpenCode provider package: `@ai-sdk/openai-compatible` + +### 2. OpenAI Responses + +- Incoming route: `POST /v1/responses` +- Discovery route: `GET /v1/models` +- OpenCode provider package: `@ai-sdk/openai` + +## Critical Scope Constraint + +Even though both protocols are OpenAI-family APIs, they are **not** interchangeable at the OpenCode config level. + +For MVP, `ops` should expose two local providers to OpenCode: + +- one chat-completions provider +- one responses provider + +This mirrors how OpenCode expects provider wiring today and avoids hidden protocol translation logic inside `ops`. + +## OpenCode Integration Strategy + +### Generated OpenCode Config + +`ops install` should rewrite the user's global OpenCode config so that OpenCode points to local proxy providers instead of raw upstream relays. + +Generated provider shape should be conceptually like this: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "ops-chat": { + "npm": "@ai-sdk/openai-compatible", + "name": "OPS Chat", + "options": { + "baseURL": "http://127.0.0.1:9982/v1", + "apiKey": "ops-local" + }, + "models": { + "gpt-5.4": { + "name": "gpt-5.4" + } + } + }, + "ops-responses": { + "npm": "@ai-sdk/openai", + "name": "OPS Responses", + "options": { + "baseURL": "http://127.0.0.1:9982/v1", + "apiKey": "ops-local" + }, + "models": { + "gpt-5.4": { + "name": "gpt-5.4" + } + } + } + }, + "model": "ops-responses/gpt-5.4", + "small_model": "ops-chat/gpt-5.4-mini" +} +``` + +### Important Rule + +`ops` should preserve as much of the user's existing OpenCode config as possible. + +`ops install` should: + +1. Back up current global config file. +2. Import provider/model information relevant to migration. +3. Preserve unrelated keys like `plugin`, `agent`, `formatter`, `permission`, `instructions`, and other user settings. +4. Replace only the parts that need to point to local proxy providers: + - `provider` + - `model` + - `small_model` + +### Project Config Limitation + +OpenCode merges config from multiple sources, and project config can override global config. + +That means `ops install` cannot guarantee full interception if a repository-local `opencode.json` or environment override replaces provider/model settings. + +MVP response: + +- document this clearly +- make `ops doctor` detect likely overrides +- support global install first + +Do **not** promise perfect takeover across all OpenCode precedence layers in MVP. + +## Migration Strategy + +### Input Sources + +`ops install` should inspect: + +1. `~/.config/opencode/opencode.json` +2. `~/.config/opencode/opencode.jsonc` +3. `~/.local/share/opencode/auth.json` when present + +Reason: + +- some users store `apiKey` inline in config +- OpenCode official flow often stores credentials in `auth.json` + +### Migration Behavior + +For each imported provider/model entry from OpenCode: + +1. Determine whether it is chat-completions-oriented or responses-oriented. +2. Create an upstream provider record in SQLite. +3. Create logical aliases from existing model names. +4. Preserve model metadata when available from config: + - display name + - limit + - attachment + - reasoning + - tool_call + - options + - variants +5. Generate local `ops-*` providers for OpenCode. + +### Protocol Classification + +Import classification rules for MVP: + +- `@ai-sdk/openai-compatible` -> `openai-chat-completions` +- `@ai-sdk/openai` -> `openai-responses` +- everything else -> unsupported for automatic migration in MVP + +If unsupported providers exist, `ops install` should warn and skip them instead of guessing. + +## SQLite-First State Model + +`ops` should not keep its own user-editable config file. + +Recommended database path: + +- Linux/WSL: `~/.local/share/ops/ops.db` +- Windows native: use `os.UserConfigDir()` or `os.UserCacheDir()`-appropriate app path, finalized in implementation + +The generated OpenCode config file is an output artifact, not `ops` source of truth. + +### Proposed Tables + +#### `providers` + +Stores upstream provider connection info. + +Suggested columns: + +- `id` +- `name` +- `protocol` +- `base_url` +- `api_key` +- `headers_json` +- `priority` +- `enabled` +- `created_at` +- `updated_at` + +#### `provider_models` + +Stores raw models discovered from upstream or imported from OpenCode config. + +Suggested columns: + +- `id` +- `provider_id` +- `remote_model_id` +- `display_name` +- `source` +- `raw_json` +- `last_seen_at` + +#### `aliases` + +Stores user-facing logical model names per protocol. + +Suggested columns: + +- `id` +- `protocol` +- `alias` +- `display_name` +- `capabilities_json` +- `limits_json` +- `options_json` +- `variants_json` +- `enabled` +- `created_at` +- `updated_at` + +#### `alias_targets` + +Maps one logical alias to one or more provider-specific targets. + +Suggested columns: + +- `id` +- `alias_id` +- `provider_id` +- `remote_model_id` +- `priority` +- `enabled` +- `created_at` +- `updated_at` + +#### `install_state` + +Tracks OpenCode integration state. + +Suggested columns: + +- `id` +- `backup_path` +- `managed_config_path` +- `installed_at` +- `last_generated_at` +- `original_config_hash` +- `generated_config_hash` + +#### `request_log` + +Optional but useful even in MVP for debugging. Keep retention small. + +Suggested columns: + +- `id` +- `protocol` +- `alias` +- `provider_id` +- `remote_model_id` +- `attempt` +- `result` +- `status_code` +- `duration_ms` +- `error_text` +- `created_at` + +## Model Alias Design + +Alias support is a core MVP feature, not a nice-to-have. + +Example: + +- logical alias: `gpt-5.4` +- responses target priority 1: provider `su8`, remote model `gpt-5.4` +- responses target priority 2: provider `codex-for-me`, remote model `GPT-5.4` +- chat target priority 1: provider `relay-x`, remote model `gpt-5.4-chat` + +This allows the user to keep using one stable model name inside OpenCode while `ops` handles provider-specific naming. + +### Important Constraint + +Aliases should be scoped by protocol. + +`gpt-5.4` for chat completions and `gpt-5.4` for responses may exist simultaneously, but they should resolve through separate alias records or a protocol-aware unique key. + +This avoids accidental cross-protocol reuse. + +## `/models` Discovery Design + +### Purpose + +Reduce manual provider setup work. + +### Behavior + +`ops` should be able to call upstream `GET /v1/models` and cache results per provider. + +Useful commands: + +- `ops provider models sync ` +- `ops provider models list ` +- `ops alias suggest ` + +### Important Limitation + +Provider `/models` data is often incomplete or inconsistent. + +It usually does **not** fully describe: + +- context length +- output limits +- attachment support +- reasoning support +- tool calling support +- custom request options + +So for MVP, `/models` must be treated as: + +- good for raw model ID discovery +- not authoritative for OpenCode capability metadata + +This means alias metadata may still need manual editing or migration from prior OpenCode config. + +## Proxy Request Flow + +### Non-Streaming Request + +1. Receive request on `/v1/chat/completions` or `/v1/responses`. +2. Parse request body enough to extract `model`. +3. Resolve `(protocol, alias)` to ordered alias targets. +4. Replace logical alias with provider-specific remote model ID. +5. Forward request to highest-priority enabled provider. +6. If response is retryable failure, try next target. +7. Return first successful response. + +### Streaming Request + +Same as above, with one critical rule: + +- failover is only allowed **before first upstream response byte is sent to the client** + +Once a stream begins successfully, `ops` must stay on that provider for that request. + +## Failover Policy + +MVP failover should stay simple and explicit. + +### Retryable Conditions + +- DNS/connect failure +- TCP reset / EOF before response +- request timeout +- chunk timeout before first byte +- HTTP `429` +- HTTP `500-599` + +### Non-Retryable Conditions + +- HTTP `400` +- HTTP `401` +- HTTP `403` +- HTTP `404` +- request validation errors from upstream after request is accepted + +### Special Case: Model Not Found + +Some providers return `404` or `400` for unknown model IDs. + +MVP should **not** build broad heuristic parsing for this. + +Reason: + +- error bodies are inconsistent +- aggressive parsing will create surprising behavior + +Safer MVP rule: + +- do not auto-fail over on ambiguous `400/404` +- surface error clearly + +This is conservative, but predictable. + +## Proxy Response Headers + +For debugging, add response headers when possible: + +- `X-OPS-Protocol` +- `X-OPS-Alias` +- `X-OPS-Provider` +- `X-OPS-Remote-Model` +- `X-OPS-Attempt` +- `X-OPS-Failover-Count` + +These headers are low-cost and help explain behavior fast. + +## Local Proxy Security Model + +MVP security posture should be intentionally modest but clear. + +### Default Behavior + +- bind only to `127.0.0.1` +- generated OpenCode config uses local static API key like `ops-local` +- proxy accepts only loopback traffic by default + +### Why This Is Acceptable For MVP + +- tool is local-only by default +- threat model is mostly accidental exposure, not multi-tenant isolation + +### Caveat + +SQLite-stored API keys are sensitive. + +MVP options: + +1. Store plaintext in SQLite with strict file permissions. +2. Defer OS keychain integration to later. + +Recommended MVP choice: + +- plaintext in SQLite +- create DB with owner-only permissions where possible +- document this explicitly + +Reason: + +- simplest implementation +- consistent cross-platform behavior +- avoids blocking MVP on secret-store complexity + +## CLI Surface + +Proposed command set: + +### Lifecycle + +- `ops init` +- `ops serve` +- `ops doctor` +- `ops install` +- `ops restore` + +### Provider Management + +- `ops provider add` +- `ops provider list` +- `ops provider edit` +- `ops provider remove` +- `ops provider enable` +- `ops provider disable` + +### Model Discovery + +- `ops provider models sync ` +- `ops provider models list ` + +### Alias Management + +- `ops alias add` +- `ops alias list` +- `ops alias bind` +- `ops alias unbind` +- `ops alias enable` +- `ops alias disable` +- `ops alias inspect ` + +### Diagnostics + +- `ops logs tail` +- `ops route test --protocol --model ` + +## Recommended Minimal UX + +Prefer explicit CLI over magical automation. + +Good path: + +1. `ops init` +2. `ops provider add` +3. `ops provider models sync` +4. `ops alias add` +5. `ops alias bind` +6. `ops install` +7. `ops serve` + +This is easy to explain and easy to debug. + +## Suggested Go Package Layout + +```text +cmd/ops/ +internal/cli/ +internal/db/ +internal/models/ +internal/providers/ +internal/aliases/ +internal/proxy/ +internal/router/ +internal/failover/ +internal/opencode/ +internal/doctor/ +internal/logging/ +``` + +### Library Choices + +- CLI: `cobra` +- SQLite driver: `modernc.org/sqlite` +- Logging: stdlib `log/slog` +- HTTP: stdlib `net/http` +- JSONC parsing for OpenCode config import/export: `tailscale/hujson` or equivalent + +Avoid adding a heavy HTTP framework unless a real need appears. + +## Generated OpenCode Provider Strategy + +MVP should generate only providers that `ops` can actually back. + +That means: + +- `ops-chat` +- `ops-responses` + +Do **not** generate fake Anthropic provider entries in MVP. + +Do **not** attempt to preserve original provider IDs inside OpenCode after installation. + +Reason: + +- `ops` becomes the stable local provider boundary +- upstream providers should move into SQLite management only + +## WSL / Windows Strategy + +This requirement is important, but it needs careful wording. + +### What MVP Should Promise + +1. Native Linux/WSL build works. +2. Native Windows build works. +3. Running OpenCode and `ops` in the **same environment** is supported. +4. `ops doctor` helps detect config-path and loopback issues. + +### What MVP Should Not Promise Yet + +1. Fully automatic cross-boundary migration between WSL OpenCode and Windows `ops`. +2. Transparent path translation for every user setup. +3. Zero-config interop when OpenCode runs on one side and proxy on the other. + +### Practical Recommendation + +For MVP, recommend users run OpenCode and `ops` in the same environment. + +Cross-environment support can be added later via explicit install target flags. + +## Biggest Risks + +### 1. OpenCode Config Precedence + +Global config rewrite alone may not capture project-level overrides. + +Mitigation: + +- `ops doctor` +- clear docs +- possible future `ops install --project` + +### 2. Provider `/models` Quality + +Discovery helps with model IDs, but not with full capability metadata. + +Mitigation: + +- preserve metadata during import +- allow manual alias metadata editing + +### 3. Streaming Edge Cases + +Once bytes are sent, failover is no longer safe. + +Mitigation: + +- fail over only before first byte +- good timeout defaults +- precise logging + +### 4. Secret Storage + +SQLite plaintext credentials are acceptable for MVP, but they are still sensitive. + +Mitigation: + +- strict file permissions +- local-only binding +- document tradeoff + +### 5. Protocol Ambiguity During Migration + +OpenCode custom providers can mix package usage in ways that are not cleanly inferable. + +Mitigation: + +- support only known package mappings in MVP +- warn instead of guessing + +## MVP Success Criteria + +MVP is successful if a user can: + +1. Import existing OpenCode provider setup into `ops`. +2. Create or verify aliases for commonly used models. +3. Install proxy-backed OpenCode global config. +4. Run `ops serve`. +5. Use OpenCode normally against `127.0.0.1:9982`. +6. Survive common upstream failures by automatic provider failover. + +## Recommended Implementation Order + +### Phase 1 + +- bootstrap Go project +- SQLite store +- provider CRUD +- alias CRUD + +### Phase 2 + +- `GET /v1/models` sync +- import from OpenCode config/auth +- generated OpenCode config writer + +### Phase 3 + +- proxy server +- chat completions forwarding +- responses forwarding +- non-streaming failover + +### Phase 4 + +- streaming support +- logging/diagnostics +- `ops doctor` +- restore flow + +## Strong Recommendation + +Do not start with background health checks, latency scoring, or dynamic routing. + +Start with deterministic priority failover only. + +That gives the product a sharp identity: + +- local +- understandable +- reliable enough +- much smaller than LiteLLM + +## Next Step + +Next practical step is implementation bootstrap for: + +1. Go module structure +2. SQLite schema migrations +3. Cobra command tree +4. OpenCode config importer/exporter + +That is the smallest slice that validates the design without touching complex proxy streaming first. diff --git a/.trellis/tasks/04-17-ops-mvp-design/task.json b/.trellis/tasks/04-17-ops-mvp-design/task.json new file mode 100644 index 0000000..1498c70 --- /dev/null +++ b/.trellis/tasks/04-17-ops-mvp-design/task.json @@ -0,0 +1,45 @@ +{ + "id": "04-17-ops-mvp-design", + "name": "OPS MVP Design", + "description": "Define MVP product and technical design for a Go-based local proxy that failsover OpenCode providers", + "status": "completed", + "dev_type": "docs", + "priority": "P1", + "creator": "Apale", + "assignee": "Apale", + "createdAt": "2026-04-17", + "completedAt": "2026-04-17", + "commit": null, + "subtasks": [ + { + "name": "Define MVP scope and non-goals", + "status": "completed" + }, + { + "name": "Define Go architecture and SQLite schema", + "status": "completed" + }, + { + "name": "Document OpenCode migration and proxy behavior", + "status": "completed" + } + ], + "children": [], + "parent": null, + "relatedFiles": [ + ".trellis/tasks/04-17-ops-mvp-design/prd.md", + "examples/opencode.jsonc" + ], + "notes": "Design-only task completed. Implementation should start from a follow-up task based on this PRD.", + "meta": { + "stack": [ + "Go", + "SQLite" + ], + "defaultPort": 9982, + "mvpProtocols": [ + "openai-chat-completions", + "openai-responses" + ] + } +}