From cf3dcec399848d8a3ee225a35a2ebdc86ae2d5ba Mon Sep 17 00:00:00 2001 From: apale7 <1092377056@qq.com> Date: Fri, 17 Apr 2026 21:43:21 +0800 Subject: [PATCH] refactor(cli): rename tool to ocswitch Avoid collision with the existing Opswitch product while keeping the repository name as opencode-provider-switch. This aligns the CLI, provider key, config paths, examples, and Trellis history under one public-facing name. --- .../04-17-forwarding-log-viewer-gui/prd.md | 34 ++-- .../04-17-forwarding-log-viewer-gui/task.json | 4 +- .../task.json | 2 +- .../prd.md | 88 ++++----- .../task.json | 4 +- .../04-17-04-17-ops-mvp-design-review/prd.md | 126 ++++++------- .../2026-04/04-17-ops-mvp-design/prd.md | 164 ++++++++--------- .../04-17-provider-disable-failover/task.json | 2 +- .trellis/workspace/Apale/index.md | 2 +- .trellis/workspace/Apale/journal-1.md | 8 +- README.md | 174 +++++++++--------- README_EN.md | 62 +++---- cmd/{olpx => ocswitch}/main.go | 4 +- examples/opencode.jsonc | 2 +- ...ode_olpx.jsonc => opencode_ocswitch.jsonc} | 14 +- go.mod | 2 +- internal/cli/alias.go | 58 +++--- internal/cli/cli_test.go | 26 +-- internal/cli/doctor.go | 22 +-- internal/cli/opencode.go | 32 ++-- internal/cli/provider.go | 66 +++---- internal/cli/root.go | 40 ++-- internal/cli/serve.go | 12 +- internal/config/config.go | 25 ++- internal/config/config_test.go | 2 +- internal/opencode/opencode.go | 103 ++++++----- internal/opencode/opencode_test.go | 162 ++++++++-------- internal/proxy/server.go | 24 +-- internal/proxy/server_test.go | 52 +++--- 29 files changed, 664 insertions(+), 652 deletions(-) rename cmd/{olpx => ocswitch}/main.go (71%) rename examples/{opencode_olpx.jsonc => opencode_ocswitch.jsonc} (81%) diff --git a/.trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md b/.trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md index 0b35cc0..1e42660 100644 --- a/.trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md +++ b/.trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md @@ -1,8 +1,8 @@ -# OLPX Forwarding Log Viewer Design +# OCSWITCH Forwarding Log Viewer Design ## Summary -`olpx` currently exposes enough per-request routing information for header-level debugging, but it does not keep a durable request log and it does not provide any local UI. +`ocswitch` currently exposes enough per-request routing information for header-level debugging, but it does not keep a durable request log and it does not provide any local UI. This task defines a minimal, implementation-ready design for a local web log viewer that shows each forwarded request with: @@ -23,18 +23,18 @@ The design is intentionally scoped to the current codebase: ### What already exists -`olpx` already has the core proxy path in `internal/proxy/server.go`: +`ocswitch` already has the core proxy path in `internal/proxy/server.go`: - `POST /v1/responses` - deterministic ordered failover - retry on transport errors, `429`, and `5xx` - no mid-stream failover after downstream response starts - debug response headers: - - `X-OLPX-Alias` - - `X-OLPX-Provider` - - `X-OLPX-Remote-Model` - - `X-OLPX-Attempt` - - `X-OLPX-Failover-Count` + - `X-OCSWITCH-Alias` + - `X-OCSWITCH-Provider` + - `X-OCSWITCH-Remote-Model` + - `X-OCSWITCH-Attempt` + - `X-OCSWITCH-Failover-Count` ### What is missing @@ -74,12 +74,12 @@ Reason: - it fits the current Go-only codebase - it avoids adding a frontend toolchain -- it can run off the same `olpx serve` process and listener +- it can run off the same `ocswitch serve` process and listener - it is the smallest path to a usable visual log surface ## High-Level Design -When `olpx serve` is running, the same HTTP server should expose three surfaces: +When `ocswitch serve` is running, the same HTTP server should expose three surfaces: 1. proxy API - `POST /v1/responses` @@ -116,10 +116,10 @@ This keeps storage simple and makes the UI directly match the user's mental mode ### Recommended MVP storage -Use an append-only JSONL file stored next to the existing `olpx` config: +Use an append-only JSONL file stored next to the existing `ocswitch` config: -- config path today: `~/.config/olpx/config.json` by default -- proposed log path: `~/.config/olpx/request-log.jsonl` +- config path today: `~/.config/ocswitch/config.json` by default +- proposed log path: `~/.config/ocswitch/request-log.jsonl` Reason: @@ -156,7 +156,7 @@ Each finalized request log entry should contain at least the following fields: "duration_ms": 1881, "protocol": "openai-responses", "alias": "gpt-5.4", - "raw_model": "olpx/gpt-5.4", + "raw_model": "ocswitch/gpt-5.4", "stream": true, "final_status": 200, "final_provider": "p2", @@ -195,7 +195,7 @@ Each finalized request log entry should contain at least the following fields: ### Required field semantics -- `alias`: normalized local alias actually routed by `olpx` +- `alias`: normalized local alias actually routed by `ocswitch` - `raw_model`: original request payload `model` value before normalization - `final_provider`: provider that produced the downstream response visible to the client - `final_remote_model`: upstream model name actually sent to that provider @@ -381,7 +381,7 @@ For MVP, the log viewer can follow the same trust model as the current local pro - same loopback listener by default - intended for single-user localhost usage -If `olpx` is later allowed to bind non-loopback addresses, the log UI and log API should be explicitly gated before reuse in that mode. +If `ocswitch` is later allowed to bind non-loopback addresses, the log UI and log API should be explicitly gated before reuse in that mode. ## Implementation Plan @@ -438,7 +438,7 @@ Suggested existing files to extend: This design is considered implemented successfully when: -1. running `olpx serve` exposes a local page at `/logs` +1. running `ocswitch serve` exposes a local page at `/logs` 2. the page shows one row per completed forwarding request 3. each row clearly shows final provider and final upstream model 4. each row clearly shows whether failover happened diff --git a/.trellis/tasks/04-17-forwarding-log-viewer-gui/task.json b/.trellis/tasks/04-17-forwarding-log-viewer-gui/task.json index 3275e0c..04708c1 100644 --- a/.trellis/tasks/04-17-forwarding-log-viewer-gui/task.json +++ b/.trellis/tasks/04-17-forwarding-log-viewer-gui/task.json @@ -1,8 +1,8 @@ { "id": "forwarding-log-viewer-gui", "name": "forwarding-log-viewer-gui", - "title": "Design forwarding log viewer GUI for olpx", - "description": "Define a minimal local web UI and request logging design for olpx forwarding operations, including provider/model visibility, token usage, and failover display.", + "title": "Design forwarding log viewer GUI for ocswitch", + "description": "Define a minimal local web UI and request logging design for ocswitch forwarding operations, including provider/model visibility, token usage, and failover display.", "status": "completed", "dev_type": "docs", "scope": "observability", diff --git a/.trellis/tasks/04-17-preserve-opencode-model-metadata-sync/task.json b/.trellis/tasks/04-17-preserve-opencode-model-metadata-sync/task.json index 64e7a4a..e9f32ca 100644 --- a/.trellis/tasks/04-17-preserve-opencode-model-metadata-sync/task.json +++ b/.trellis/tasks/04-17-preserve-opencode-model-metadata-sync/task.json @@ -1,7 +1,7 @@ { "id": "preserve-opencode-model-metadata-sync", "name": "preserve-opencode-model-metadata-sync", - "title": "Preserve OpenCode model metadata during olpx sync", + "title": "Preserve OpenCode model metadata during ocswitch sync", "description": "", "status": "completed", "dev_type": null, diff --git a/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/prd.md b/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/prd.md index e19a234..8ba61d5 100644 --- a/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/prd.md +++ b/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/prd.md @@ -1,25 +1,25 @@ -# OLPX Agent-First CLI Help System +# OCSWITCH Agent-First CLI Help System ## Summary -`olpx` already has a working CLI and a reasonably complete README, but its help +`ocswitch` already has a working CLI and a reasonably complete README, but its help surface is still optimized for a human reading source-adjacent docs, not for an AI agent trying to configure the tool end-to-end with high reliability. This PRD defines a narrow product change: -- every user-facing `olpx` command must provide `Long` help -- every user-facing `olpx` command must provide `Example` help +- every user-facing `ocswitch` command must provide `Long` help +- every user-facing `ocswitch` command must provide `Example` help - help content must be written for **AI agent execution reliability first** - manual CLI ergonomics remain important, but are secondary to agent clarity -The intended result is that an AI agent can use `olpx --help` and nested +The intended result is that an AI agent can use `ocswitch --help` and nested `--help` output as a trustworthy local interface contract for discovery, planning, execution, and recovery during configuration. ## Product Goal -Make `olpx` self-describing enough that a capable AI agent can complete normal +Make `ocswitch` self-describing enough that a capable AI agent can complete normal configuration workflows without relying on hidden tribal knowledge, source code inspection, or README-only instructions. @@ -27,7 +27,7 @@ inspection, or README-only instructions. User wants to say, in effect: -"Configure `olpx` for my providers and aliases, then sync OpenCode and tell me +"Configure `ocswitch` for my providers and aliases, then sync OpenCode and tell me how to run it." The agent should be able to do that safely by reading CLI help output and @@ -39,9 +39,9 @@ Current repository state already supports the core workflow: - add or import upstream providers - create aliases and bind ordered targets -- validate config via `olpx doctor` -- sync aliases into OpenCode via `olpx opencode sync` -- run proxy via `olpx serve` +- validate config via `ocswitch doctor` +- sync aliases into OpenCode via `ocswitch opencode sync` +- run proxy via `ocswitch serve` But the command tree does not yet expose the full operational contract through help text. In practice this means: @@ -52,7 +52,7 @@ help text. In practice this means: - the "what to do next" step is often only in README, not in command help - an agent may need to infer workflow rules from source code instead of help -This is acceptable for an engineering MVP, but not good enough if `olpx` wants +This is acceptable for an engineering MVP, but not good enough if `ocswitch` wants to recommend agent-driven configuration as a first-class path. ## Design Principle @@ -97,22 +97,22 @@ This means slightly longer help is acceptable if it reduces agent mistakes. All user-facing commands in the current Cobra tree: -- `olpx` -- `olpx serve` -- `olpx doctor` -- `olpx provider` -- `olpx provider add` -- `olpx provider list` -- `olpx provider remove` -- `olpx provider import-opencode` -- `olpx alias` -- `olpx alias add` -- `olpx alias list` -- `olpx alias bind` -- `olpx alias unbind` -- `olpx alias remove` -- `olpx opencode` -- `olpx opencode sync` +- `ocswitch` +- `ocswitch serve` +- `ocswitch doctor` +- `ocswitch provider` +- `ocswitch provider add` +- `ocswitch provider list` +- `ocswitch provider remove` +- `ocswitch provider import-opencode` +- `ocswitch alias` +- `ocswitch alias add` +- `ocswitch alias list` +- `ocswitch alias bind` +- `ocswitch alias unbind` +- `ocswitch alias remove` +- `ocswitch opencode` +- `ocswitch opencode sync` ### Also In Scope @@ -169,7 +169,7 @@ Examples must avoid: ### Requirement 3: Root and group commands must explain workflow position -Commands like `olpx`, `olpx provider`, `olpx alias`, and `olpx opencode` are not +Commands like `ocswitch`, `ocswitch provider`, `ocswitch alias`, and `ocswitch opencode` are not action commands themselves, but they are decision points for an agent. Their help must answer: @@ -184,8 +184,8 @@ Whenever a command writes local config or OpenCode config, help must say so. Examples: -- provider commands write `olpx` config -- alias commands write `olpx` config +- provider commands write `ocswitch` config +- alias commands write `ocswitch` config - `opencode sync` writes the target OpenCode config unless `--dry-run` - `doctor` validates statically and does not call upstream providers - `serve` starts a long-running local proxy and does not mutate config @@ -197,7 +197,7 @@ defaults. Examples of defaults that must appear where relevant: -- default `olpx` config path via `--config` +- default `ocswitch` config path via `--config` - default OpenCode sync target resolution order - default proxy bind address and API key when describing `serve` or workflows @@ -209,7 +209,7 @@ config files to chat or expose API keys in logs. ## Content Contract By Command Type -### Root Command: `olpx` +### Root Command: `ocswitch` `Long` should define the full happy-path workflow: @@ -229,7 +229,7 @@ config files to chat or expose API keys in logs. `Long` should explain that providers are upstream OpenAI-compatible endpoints used by alias targets. It should state that provider definitions live in local -`olpx` config and are separate from alias routing. +`ocswitch` config and are separate from alias routing. `Example` should include: @@ -268,7 +268,7 @@ used by agents to confirm imported or saved provider IDs before binding aliases. `Long` should explain: -- removes provider from `olpx` config +- removes provider from `ocswitch` config - does not automatically clean alias references - follow-up `doctor` may fail if aliases still reference removed provider @@ -296,7 +296,7 @@ used by agents to confirm imported or saved provider IDs before binding aliases. ### Group Command: `alias` `Long` should explain aliases as the primary user-facing abstraction exposed to -OpenCode as `olpx/`, and that target order defines failover priority. +OpenCode as `ocswitch/`, and that target order defines failover priority. `Example` should include: @@ -366,7 +366,7 @@ OpenCode as `olpx/`, and that target order defines failover priority. `Long` should explain: - removes entire alias from local config -- future `opencode sync` will stop exposing it in `provider.olpx.models` +- future `opencode sync` will stop exposing it in `provider.ocswitch.models` - does not directly remove model selection already set elsewhere in OpenCode `Example` should include: @@ -391,7 +391,7 @@ OpenCode as `olpx/`, and that target order defines failover priority. ### Group Command: `opencode` `Long` should explain that these commands manage the narrow integration boundary -between `olpx` and OpenCode, and do not attempt full OpenCode config takeover. +between `ocswitch` and OpenCode, and do not attempt full OpenCode config takeover. `Example` should include: @@ -407,7 +407,7 @@ contract. - exact write target rules - what fields are mutated and not mutated -- that aliases become `provider.olpx.models` +- that aliases become `provider.ocswitch.models` - meaning of `--dry-run` - recommended call order around `doctor` @@ -481,7 +481,7 @@ scenarios without README-only dependency. ### Scenario 1: Configure from scratch -1. inspect `olpx --help` +1. inspect `ocswitch --help` 2. add one or more providers 3. add alias 4. bind targets in order @@ -491,7 +491,7 @@ scenarios without README-only dependency. ### Scenario 2: Import existing OpenCode provider definitions first -1. inspect `olpx provider import-opencode --help` +1. inspect `ocswitch provider import-opencode --help` 2. import supported providers 3. list providers 4. create aliases and bindings @@ -527,9 +527,9 @@ README should not be the only place where critical command semantics live. ### Functional Acceptance -1. Every user-facing Cobra command in the current `olpx` tree defines non-empty +1. Every user-facing Cobra command in the current `ocswitch` tree defines non-empty `Long` and non-empty `Example` help. -2. `olpx --help` and all nested `--help` pages expose enough information for an +2. `ocswitch --help` and all nested `--help` pages expose enough information for an agent to discover the intended configuration workflow without reading source. 3. Help text for mutating commands explicitly states what files/config are written. @@ -593,14 +593,14 @@ duplication becomes genuinely harmful. These are explicitly out of this task, but compatible with it later: -- `olpx setup` guided workflow command +- `ocswitch setup` guided workflow command - shell completion tuned for provider/alias names - structured `--help-format json` for agent-native consumption - README agent prompt template that mirrors the help contract ## Final Product Decision -`olpx` should recommend an **agent-first, CLI-native** configuration workflow. +`ocswitch` should recommend an **agent-first, CLI-native** configuration workflow. The CLI itself must become the most reliable place to learn how to configure the tool. `Long` and `Example` are therefore not documentation polish; they are part diff --git a/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/task.json b/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/task.json index 6004d71..01e3e9d 100644 --- a/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/task.json +++ b/.trellis/tasks/archive/2026-04/04-17-04-17-agent-first-cli-help-prd/task.json @@ -2,7 +2,7 @@ "id": "04-17-agent-first-cli-help-prd", "name": "04-17-agent-first-cli-help-prd", "title": "PRD: agent-first CLI help system", - "description": "Define an agent-first product spec for olpx CLI Long/Example help and related docs behavior", + "description": "Define an agent-first product spec for ocswitch CLI Long/Example help and related docs behavior", "status": "completed", "dev_type": "docs", "scope": null, @@ -57,7 +57,7 @@ "internal/cli/serve.go", "README.md" ], - "notes": "This task defines an agent-first PRD for making olpx CLI help text a reliable configuration interface for AI agents.", + "notes": "This task defines an agent-first PRD for making ocswitch CLI help text a reliable configuration interface for AI agents.", "meta": { "primaryAudience": "ai-agent", "priorityRule": "agent-first-manual-second", diff --git a/.trellis/tasks/archive/2026-04/04-17-04-17-ops-mvp-design-review/prd.md b/.trellis/tasks/archive/2026-04/04-17-04-17-ops-mvp-design-review/prd.md index dd65249..7798458 100644 --- a/.trellis/tasks/archive/2026-04/04-17-04-17-ops-mvp-design-review/prd.md +++ b/.trellis/tasks/archive/2026-04/04-17-04-17-ops-mvp-design-review/prd.md @@ -1,8 +1,8 @@ -# OLPX MVP Redesign +# OCSWITCH MVP Redesign ## Summary -`opencode-provider-switch` (`olpx`) is a local proxy for OpenCode focused on one narrow job: +`opencode-provider-switch` (`ocswitch`) 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 @@ -15,7 +15,7 @@ MVP is about one thing: **multi-provider failover behind a stable OpenCode model ## Product Goal -When a user chooses `olpx/` inside OpenCode, `olpx` 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. +When a user chooses `ocswitch/` inside OpenCode, `ocswitch` 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 @@ -56,13 +56,13 @@ User wants: ## Architecture in One Sentence -OpenCode sends `POST /v1/responses` to local provider `olpx`; `olpx` resolves requested alias to an ordered target list and proxies request to first healthy upstream candidate. +OpenCode sends `POST /v1/responses` to local provider `ocswitch`; `ocswitch` resolves requested alias to an ordered target list and proxies request to first healthy upstream candidate. ## High-Level Architecture ```text OpenCode - -> custom provider `olpx` (@ai-sdk/openai) + -> custom provider `ocswitch` (@ai-sdk/openai) -> http://127.0.0.1:9982/v1/responses -> alias resolver -> failover engine @@ -77,10 +77,10 @@ OpenCode MVP should expose exactly one local provider to OpenCode: -- provider id: `olpx` +- provider id: `ocswitch` - npm package: `@ai-sdk/openai` - base URL: `http://127.0.0.1:9982/v1` -- local API key: static placeholder such as `olpx-local` +- local API key: static placeholder such as `ocswitch-local` Conceptual OpenCode config shape: @@ -88,12 +88,12 @@ Conceptual OpenCode config shape: { "$schema": "https://opencode.ai/config.json", "provider": { - "olpx": { + "ocswitch": { "npm": "@ai-sdk/openai", "name": "OPS", "options": { "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local" + "apiKey": "ocswitch-local" }, "models": { "gpt-5.4": { @@ -105,7 +105,7 @@ Conceptual OpenCode config shape: } } }, - "model": "olpx/gpt-5.4" + "model": "ocswitch/gpt-5.4" } ``` @@ -117,23 +117,23 @@ OpenCode source confirms this path is valid. 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 `olpx` does **not** need a special external model catalog protocol for MVP. +OpenCode provider loading also merges custom config-defined providers and models into runtime provider state. That means `ocswitch` does **not** need a special external model catalog protocol for MVP. Simplest MVP path: -- keep alias list in `olpx` -- sync alias list into OpenCode `provider.olpx.models` -- make sure `provider.olpx` is valid enough to appear as a connected runtime provider -- let OpenCode surface `olpx/` in `/models` and `/model` +- keep alias list in `ocswitch` +- sync alias list into OpenCode `provider.ocswitch.models` +- make sure `provider.ocswitch` is valid enough to appear as a connected runtime provider +- let OpenCode surface `ocswitch/` in `/models` and `/model` ### Important Scope Rule MVP should **not** rewrite the entire OpenCode config. -Instead, `olpx` should only support a narrow integration step: +Instead, `ocswitch` should only support a narrow integration step: -- ensure `provider.olpx` exists or is updated -- optionally sync alias entries into `provider.olpx.models` +- ensure `provider.ocswitch` exists or is updated +- optionally sync alias entries into `provider.ocswitch.models` - optionally let user set `model` or `small_model` manually This avoids the previous PRD's high-risk install/restore workflow. @@ -144,7 +144,7 @@ OpenCode source also confirms config is merged from multiple layers, and lower-p Implication for MVP: -- `olpx opencode sync` must know which config file it is targeting +- `ocswitch 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` @@ -153,11 +153,11 @@ Implication for MVP: ## Provider Source Model -`olpx` needs upstream provider definitions, but this is no longer the product center. +`ocswitch` needs upstream provider definitions, but this is no longer the product center. MVP should support two input paths: -1. manual provider entry through `olpx` CLI +1. manual provider entry through `ocswitch` CLI 2. one-shot import from one explicit OpenCode config file, defaulting to global user config ### Supported Provider Shape In MVP @@ -179,7 +179,7 @@ OpenCode source also shows that `auth.json` provides credentials for an existing Implication for MVP: -- `olpx` should import provider definitions from config, not from merged runtime auth state +- `ocswitch` 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 @@ -200,8 +200,8 @@ User should use alias directly inside OpenCode. User should not need to know con 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 `olpx`. -5. OpenCode should reference alias as `olpx/`. +4. Alias name must be unique within `ocswitch`. +5. OpenCode should reference alias as `ocswitch/`. ### Example @@ -234,7 +234,7 @@ Rich capability metadata such as `limit`, `attachment`, `reasoning`, `tool_call` 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 `olpx` alias. +4. Treat `model` as `ocswitch` 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. @@ -303,13 +303,13 @@ Conservative failover is preferable to surprising failover. ## Proxy Debugging Headers -For debugging, `olpx` should add response headers where possible: +For debugging, `ocswitch` should add response headers where possible: -- `X-OLPX-Alias` -- `X-OLPX-Provider` -- `X-OLPX-Remote-Model` -- `X-OLPX-Attempt` -- `X-OLPX-Failover-Count` +- `X-OCSWITCH-Alias` +- `X-OCSWITCH-Provider` +- `X-OCSWITCH-Remote-Model` +- `X-OCSWITCH-Attempt` +- `X-OCSWITCH-Failover-Count` These headers are cheap and make failover behavior understandable. @@ -321,7 +321,7 @@ MVP should prefer a simpler user-editable config shape unless implementation pro ### Recommended MVP Direction -Use one local `olpx` JSON or JSONC config file for: +Use one local `ocswitch` JSON or JSONC config file for: - upstream providers - aliases @@ -341,19 +341,19 @@ Recommended MVP commands: ### Core -- `olpx serve` -- `olpx doctor` +- `ocswitch serve` +- `ocswitch doctor` -### `olpx doctor` MVP Boundary +### `ocswitch doctor` MVP Boundary -First-release `olpx doctor` should stay side-effect free. +First-release `ocswitch doctor` should stay side-effect free. It should validate: -- local `olpx` config can be loaded +- local `ocswitch` 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.olpx` config shape is structurally valid +- generated or synced `provider.ocswitch` config shape is structurally valid It should not, by default: @@ -361,35 +361,35 @@ It should not, by default: - 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 `olpx doctor --live`. +If live upstream probing is needed later, it should be added as explicit opt-in behavior such as `ocswitch doctor --live`. ### Provider Management -- `olpx provider add` -- `olpx provider list` -- `olpx provider remove` -- `olpx provider import-opencode` +- `ocswitch provider add` +- `ocswitch provider list` +- `ocswitch provider remove` +- `ocswitch provider import-opencode` ### Alias Management -- `olpx alias add` -- `olpx alias list` -- `olpx alias bind` -- `olpx alias unbind` -- `olpx alias remove` +- `ocswitch alias add` +- `ocswitch alias list` +- `ocswitch alias bind` +- `ocswitch alias unbind` +- `ocswitch alias remove` ### OpenCode Integration -- `olpx opencode sync` +- `ocswitch opencode sync` -## `olpx opencode sync` Responsibility +## `ocswitch opencode sync` Responsibility This command should do one narrow job: -- by default update or create custom provider `olpx` in global OpenCode user config +- by default update or create custom provider `ocswitch` 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.olpx.models` +- sync current alias names into `provider.ocswitch.models` Optional extra behavior: @@ -418,15 +418,15 @@ 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.olpx.models` is sufficient for exposure if `provider.olpx` lands in connected runtime provider state +- syncing alias keys into `provider.ocswitch.models` is sufficient for exposure if `provider.ocswitch` 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 `olpx` -- store upstream credentials in local `olpx` config +- use static local placeholder API key between OpenCode and `ocswitch` +- store upstream credentials in local `ocswitch` config - document that local credential storage is sensitive No multi-user or remote-network security guarantees in MVP. @@ -437,9 +437,9 @@ 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 `olpx opencode sync` and see alias names appear in OpenCode `opencode models` output and `/model` picker -4. select `olpx/` in OpenCode without exposing concrete upstream model IDs -5. send normal streaming OpenAI Responses traffic through `olpx` +3. run `ocswitch opencode sync` and see alias names appear in OpenCode `opencode models` output and `/model` picker +4. select `ocswitch/` in OpenCode without exposing concrete upstream model IDs +5. send normal streaming OpenAI Responses traffic through `ocswitch` 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 @@ -454,10 +454,10 @@ MVP is successful if a user can: ### Phase 2 -- `olpx opencode sync` +- `ocswitch opencode sync` - narrow OpenCode provider import - alias list exposure in OpenCode config -- connected-provider validation in `olpx doctor` +- connected-provider validation in `ocswitch doctor` ### Phase 3 @@ -469,16 +469,16 @@ MVP is successful if a user can: ### Phase 4 -- `olpx doctor` +- `ocswitch doctor` - debugging headers and logs ## Finalized MVP Decisions The following implementation choices are now locked for first-release MVP: -1. `olpx opencode sync` updates `provider.olpx` and alias exposure only by default. It must not modify OpenCode `model` or `small_model` unless explicit opt-in flags are provided. +1. `ocswitch opencode sync` updates `provider.ocswitch` 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. `olpx doctor` is static by default and must not issue live upstream requests unless future explicit opt-in behavior is added. +3. `ocswitch doctor` is static by default and must not issue live upstream requests unless future explicit opt-in behavior is added. ## Strong Recommendation diff --git a/.trellis/tasks/archive/2026-04/04-17-ops-mvp-design/prd.md b/.trellis/tasks/archive/2026-04/04-17-ops-mvp-design/prd.md index b965284..bc43e47 100644 --- a/.trellis/tasks/archive/2026-04/04-17-ops-mvp-design/prd.md +++ b/.trellis/tasks/archive/2026-04/04-17-ops-mvp-design/prd.md @@ -1,8 +1,8 @@ -# OLPX MVP Design +# OCSWITCH MVP Design ## Summary -`opencode-provider-switch` (`olpx`) is a local CLI + proxy for OpenCode. +`opencode-provider-switch` (`ocswitch`) is a local CLI + proxy for OpenCode. Its job is narrow: @@ -10,7 +10,7 @@ Its job is narrow: - 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 `olpx` config file +- Manage state in SQLite, not in an `ocswitch` 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. @@ -32,7 +32,7 @@ MVP intentionally does **not** try to become a general AI gateway, a dashboard p ## Product Goals 1. Keep OpenCode usable when cheap relay providers fail intermittently. -2. Reduce OpenCode config complexity by centralizing provider management in `olpx`. +2. Reduce OpenCode config complexity by centralizing provider management in `ocswitch`. 3. Make failover behavior predictable, visible, and debuggable. 4. Preserve OpenCode-native workflow instead of asking users to switch tools. @@ -57,9 +57,9 @@ Go is the right fit for this product shape: ## Core Design Principle -`olpx` should manage **protocol pools** and **logical aliases**, not raw provider selection inside OpenCode. +`ocswitch` should manage **protocol pools** and **logical aliases**, not raw provider selection inside OpenCode. -OpenCode should see a small number of local proxy providers. `olpx` should own: +OpenCode should see a small number of local proxy providers. `ocswitch` should own: - upstream providers - API keys and headers @@ -74,7 +74,7 @@ This keeps OpenCode config stable even when upstream providers are added, remove ```text OpenCode -> local OpenAI-compatible provider config - -> olpx proxy (127.0.0.1:9982) + -> ocswitch proxy (127.0.0.1:9982) -> protocol router -> alias resolver -> failover engine @@ -127,18 +127,18 @@ OpenCode Even though both protocols are OpenAI-family APIs, they are **not** interchangeable at the OpenCode config level. -For MVP, `olpx` should expose two local providers to OpenCode: +For MVP, `ocswitch` 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 `olpx`. +This mirrors how OpenCode expects provider wiring today and avoids hidden protocol translation logic inside `ocswitch`. ## OpenCode Integration Strategy ### Generated OpenCode Config -`olpx install` should rewrite the user's global OpenCode config so that OpenCode points to local proxy providers instead of raw upstream relays. +`ocswitch 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: @@ -146,12 +146,12 @@ Generated provider shape should be conceptually like this: { "$schema": "https://opencode.ai/config.json", "provider": { - "olpx-chat": { + "ocswitch-chat": { "npm": "@ai-sdk/openai-compatible", - "name": "OLPX Chat", + "name": "OCSWITCH Chat", "options": { "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local" + "apiKey": "ocswitch-local" }, "models": { "gpt-5.4": { @@ -159,12 +159,12 @@ Generated provider shape should be conceptually like this: } } }, - "olpx-responses": { + "ocswitch-responses": { "npm": "@ai-sdk/openai", - "name": "OLPX Responses", + "name": "OCSWITCH Responses", "options": { "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local" + "apiKey": "ocswitch-local" }, "models": { "gpt-5.4": { @@ -173,16 +173,16 @@ Generated provider shape should be conceptually like this: } } }, - "model": "olpx-responses/gpt-5.4", - "small_model": "olpx-chat/gpt-5.4-mini" + "model": "ocswitch-responses/gpt-5.4", + "small_model": "ocswitch-chat/gpt-5.4-mini" } ``` ### Important Rule -`olpx` should preserve as much of the user's existing OpenCode config as possible. +`ocswitch` should preserve as much of the user's existing OpenCode config as possible. -`olpx install` should: +`ocswitch install` should: 1. Back up current global config file. 2. Import provider/model information relevant to migration. @@ -196,12 +196,12 @@ Generated provider shape should be conceptually like this: OpenCode merges config from multiple sources, and project config can override global config. -That means `olpx install` cannot guarantee full interception if a repository-local `opencode.json` or environment override replaces provider/model settings. +That means `ocswitch install` cannot guarantee full interception if a repository-local `opencode.json` or environment override replaces provider/model settings. MVP response: - document this clearly -- make `olpx doctor` detect likely overrides +- make `ocswitch doctor` detect likely overrides - support global install first Do **not** promise perfect takeover across all OpenCode precedence layers in MVP. @@ -210,7 +210,7 @@ Do **not** promise perfect takeover across all OpenCode precedence layers in MVP ### Input Sources -`olpx install` should inspect: +`ocswitch install` should inspect: 1. `~/.config/opencode/opencode.json` 2. `~/.config/opencode/opencode.jsonc` @@ -236,7 +236,7 @@ For each imported provider/model entry from OpenCode: - tool_call - options - variants -5. Generate local `olpx-*` providers for OpenCode. +5. Generate local `ocswitch-*` providers for OpenCode. ### Protocol Classification @@ -246,18 +246,18 @@ Import classification rules for MVP: - `@ai-sdk/openai` -> `openai-responses` - everything else -> unsupported for automatic migration in MVP -If unsupported providers exist, `olpx install` should warn and skip them instead of guessing. +If unsupported providers exist, `ocswitch install` should warn and skip them instead of guessing. ## SQLite-First State Model -`olpx` should not keep its own user-editable config file. +`ocswitch` should not keep its own user-editable config file. Recommended database path: -- Linux/WSL: `~/.local/share/olpx/olpx.db` +- Linux/WSL: `~/.local/share/ocswitch/ocswitch.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 `olpx` source of truth. +The generated OpenCode config file is an output artifact, not `ocswitch` source of truth. ### Proposed Tables @@ -368,7 +368,7 @@ Example: - 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 `olpx` handles provider-specific naming. +This allows the user to keep using one stable model name inside OpenCode while `ocswitch` handles provider-specific naming. ### Important Constraint @@ -386,13 +386,13 @@ Reduce manual provider setup work. ### Behavior -`olpx` should be able to call upstream `GET /v1/models` and cache results per provider. +`ocswitch` should be able to call upstream `GET /v1/models` and cache results per provider. Useful commands: -- `olpx provider models sync ` -- `olpx provider models list ` -- `olpx alias suggest ` +- `ocswitch provider models sync ` +- `ocswitch provider models list ` +- `ocswitch alias suggest ` ### Important Limitation @@ -432,7 +432,7 @@ 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, `olpx` must stay on that provider for that request. +Once a stream begins successfully, `ocswitch` must stay on that provider for that request. ## Failover Policy @@ -477,12 +477,12 @@ This is conservative, but predictable. For debugging, add response headers when possible: -- `X-OLPX-Protocol` -- `X-OLPX-Alias` -- `X-OLPX-Provider` -- `X-OLPX-Remote-Model` -- `X-OLPX-Attempt` -- `X-OLPX-Failover-Count` +- `X-OCSWITCH-Protocol` +- `X-OCSWITCH-Alias` +- `X-OCSWITCH-Provider` +- `X-OCSWITCH-Remote-Model` +- `X-OCSWITCH-Attempt` +- `X-OCSWITCH-Failover-Count` These headers are low-cost and help explain behavior fast. @@ -493,7 +493,7 @@ 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 `olpx-local` +- generated OpenCode config uses local static API key like `ocswitch-local` - proxy accepts only loopback traffic by default ### Why This Is Acceptable For MVP @@ -528,40 +528,40 @@ Proposed command set: ### Lifecycle -- `olpx init` -- `olpx serve` -- `olpx doctor` -- `olpx install` -- `olpx restore` +- `ocswitch init` +- `ocswitch serve` +- `ocswitch doctor` +- `ocswitch install` +- `ocswitch restore` ### Provider Management -- `olpx provider add` -- `olpx provider list` -- `olpx provider edit` -- `olpx provider remove` -- `olpx provider enable` -- `olpx provider disable` +- `ocswitch provider add` +- `ocswitch provider list` +- `ocswitch provider edit` +- `ocswitch provider remove` +- `ocswitch provider enable` +- `ocswitch provider disable` ### Model Discovery -- `olpx provider models sync ` -- `olpx provider models list ` +- `ocswitch provider models sync ` +- `ocswitch provider models list ` ### Alias Management -- `olpx alias add` -- `olpx alias list` -- `olpx alias bind` -- `olpx alias unbind` -- `olpx alias enable` -- `olpx alias disable` -- `olpx alias inspect ` +- `ocswitch alias add` +- `ocswitch alias list` +- `ocswitch alias bind` +- `ocswitch alias unbind` +- `ocswitch alias enable` +- `ocswitch alias disable` +- `ocswitch alias inspect ` ### Diagnostics -- `olpx logs tail` -- `olpx route test --protocol --model ` +- `ocswitch logs tail` +- `ocswitch route test --protocol --model ` ## Recommended Minimal UX @@ -569,20 +569,20 @@ Prefer explicit CLI over magical automation. Good path: -1. `olpx init` -2. `olpx provider add` -3. `olpx provider models sync` -4. `olpx alias add` -5. `olpx alias bind` -6. `olpx install` -7. `olpx serve` +1. `ocswitch init` +2. `ocswitch provider add` +3. `ocswitch provider models sync` +4. `ocswitch alias add` +5. `ocswitch alias bind` +6. `ocswitch install` +7. `ocswitch serve` This is easy to explain and easy to debug. ## Suggested Go Package Layout ```text -cmd/olpx/ +cmd/ocswitch/ internal/cli/ internal/db/ internal/models/ @@ -608,12 +608,12 @@ Avoid adding a heavy HTTP framework unless a real need appears. ## Generated OpenCode Provider Strategy -MVP should generate only providers that `olpx` can actually back. +MVP should generate only providers that `ocswitch` can actually back. That means: -- `olpx-chat` -- `olpx-responses` +- `ocswitch-chat` +- `ocswitch-responses` Do **not** generate fake Anthropic provider entries in MVP. @@ -621,7 +621,7 @@ Do **not** attempt to preserve original provider IDs inside OpenCode after insta Reason: -- `olpx` becomes the stable local provider boundary +- `ocswitch` becomes the stable local provider boundary - upstream providers should move into SQLite management only ## WSL / Windows Strategy @@ -632,18 +632,18 @@ This requirement is important, but it needs careful wording. 1. Native Linux/WSL build works. 2. Native Windows build works. -3. Running OpenCode and `olpx` in the **same environment** is supported. -4. `olpx doctor` helps detect config-path and loopback issues. +3. Running OpenCode and `ocswitch` in the **same environment** is supported. +4. `ocswitch 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 `olpx`. +1. Fully automatic cross-boundary migration between WSL OpenCode and Windows `ocswitch`. 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 `olpx` in the same environment. +For MVP, recommend users run OpenCode and `ocswitch` in the same environment. Cross-environment support can be added later via explicit install target flags. @@ -655,7 +655,7 @@ Global config rewrite alone may not capture project-level overrides. Mitigation: -- `olpx doctor` +- `ocswitch doctor` - clear docs - possible future `ops install --project` @@ -704,7 +704,7 @@ 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 `olpx serve`. +4. Run `ocswitch serve`. 5. Use OpenCode normally against `127.0.0.1:9982`. 6. Survive common upstream failures by automatic provider failover. @@ -734,7 +734,7 @@ MVP is successful if a user can: - streaming support - logging/diagnostics -- `olpx doctor` +- `ocswitch doctor` - restore flow ## Strong Recommendation diff --git a/.trellis/tasks/archive/2026-04/04-17-provider-disable-failover/task.json b/.trellis/tasks/archive/2026-04/04-17-provider-disable-failover/task.json index 441c369..f502ede 100644 --- a/.trellis/tasks/archive/2026-04/04-17-provider-disable-failover/task.json +++ b/.trellis/tasks/archive/2026-04/04-17-provider-disable-failover/task.json @@ -1,7 +1,7 @@ { "id": "provider-disable-failover", "name": "provider-disable-failover", - "title": "Support disabling providers in olpx", + "title": "Support disabling providers in ocswitch", "description": "", "status": "completed", "dev_type": null, diff --git a/.trellis/workspace/Apale/index.md b/.trellis/workspace/Apale/index.md index 96f0b0b..3dcaa39 100644 --- a/.trellis/workspace/Apale/index.md +++ b/.trellis/workspace/Apale/index.md @@ -31,7 +31,7 @@ |---|------|-------|---------|--------| | 5 | 2026-04-17 | Fix sync command panic | `887eb14` | `master` | | 4 | 2026-04-17 | Review MVP completion status | - | `master` | -| 3 | 2026-04-17 | Support disabling providers in olpx | HEAD | `master` | +| 3 | 2026-04-17 | Support disabling providers in ocswitch | HEAD | `master` | | 2 | 2026-04-17 | Build OPS MVP failover proxy | `2b04d91` | `master` | | 1 | 2026-04-17 | Finalize OPS MVP review PRD | `eeacdc4`, `00747fa`, `ca0b3c4` | `master` | diff --git a/.trellis/workspace/Apale/journal-1.md b/.trellis/workspace/Apale/journal-1.md index 3bac283..b970f0f 100644 --- a/.trellis/workspace/Apale/journal-1.md +++ b/.trellis/workspace/Apale/journal-1.md @@ -91,10 +91,10 @@ Implemented the Go-based OPS MVP: local config, provider and alias CLI, OpenCode - None - task complete -## Session 3: Support disabling providers in olpx +## Session 3: Support disabling providers in ocswitch **Date**: 2026-04-17 -**Task**: Support disabling providers in olpx +**Task**: Support disabling providers in ocswitch **Branch**: `master` ### Summary @@ -139,7 +139,7 @@ Reviewed current implementation against the archived MVP PRD and classified impl | Category | Result | |---|---| | Overall status | MVP core flow is effectively complete | -| Implemented MVP | Local olpx config, provider/alias CLI, OpenCode sync, static doctor, `/v1/responses` proxy, streaming pass-through, pre-first-byte failover, debug headers | +| Implemented MVP | Local ocswitch config, provider/alias CLI, OpenCode sync, static doctor, `/v1/responses` proxy, streaming pass-through, pre-first-byte failover, debug headers | | Partial / missing MVP edges | `doctor` validates generated preview more than on-disk synced state; OpenCode provider import only reads `options.apiKey`, not broader header-style auth | | Beyond MVP | `provider enable/disable`, minimal `/v1/models`, broader alias normalization, careful OpenCode config patching | @@ -191,7 +191,7 @@ Fixed a panic in opencode sync when preserved model metadata contains slices, an ### Main Changes -- Root cause: `internal/opencode/opencode.go` compared `interface{}` values directly inside `mapsEqualShallow`, which panicked when preserved `provider.olpx.models.` metadata included slices. +- Root cause: `internal/opencode/opencode.go` compared `interface{}` values directly inside `mapsEqualShallow`, which panicked when preserved `provider.ocswitch.models.` metadata included slices. - Fix: replaced the unsafe hand-rolled comparison with `reflect.DeepEqual` so existing model metadata with nested maps/slices can be compared safely during `sync`. - Added regression tests in `internal/opencode/opencode_test.go` and `internal/cli/cli_test.go` covering preserved slice metadata and a real `opencode sync --target` no-op path. - Verification: ran `rtk go test ./internal/opencode`, `rtk go test ./internal/cli ./internal/opencode`, and `rtk go test ./...` successfully. diff --git a/README.md b/README.md index 56aedf1..f617d3a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# opencode-provider-switch (`olpx`) +# opencode-provider-switch (`ocswitch`) English README: `README_EN.md` -`olpx` 是 OpenCode LocalProxy CLI,给 OpenCode 使用的本地代理。 +`ocswitch` 是 OpenCode Provider Switch CLI,给 OpenCode 使用的本地代理。 它解决的问题很简单: -- 你在 OpenCode 里只使用一个稳定的模型名,例如 `olpx/gpt-5.4` -- `olpx` 在本地把这个别名映射到多个上游 `provider/model` +- 你在 OpenCode 里只使用一个稳定的模型名,例如 `ocswitch/gpt-5.4` +- `ocswitch` 在本地把这个别名映射到多个上游 `provider/model` - 按你配置的顺序依次尝试上游 - 如果主上游在响应开始前失败,自动切到下一个上游 @@ -16,17 +16,17 @@ English README: `README_EN.md` ## 适合什么场景 - 你有多个 OpenAI 兼容上游 -- 你不想在 OpenCode 里频繁切换 provider +- 你不想在 OpenCode 里频繁切换 provider 或模型入口 - 你希望用一个固定别名承接多个备用上游 - 你希望失败切换行为是确定的、可预期的 ## 当前能力 -- 本地维护 `olpx` 配置文件:上游 provider、alias、监听地址 +- 本地维护 `ocswitch` 配置文件:上游 provider、alias、监听地址 - 支持手动添加 provider - 支持从 OpenCode 配置导入 `@ai-sdk/openai` 自定义 provider - 支持创建 alias,并按顺序绑定多个上游 target -- 支持把 alias 同步到 OpenCode 的 `provider.olpx.models` +- 支持把 alias 同步到 OpenCode 的 `provider.ocswitch.models` - 支持本地代理 `POST /v1/responses` - 支持流式透传 - 支持首字节前失败切换 @@ -45,30 +45,30 @@ English README: `README_EN.md` ## 安装 ```bash -go build -o olpx ./cmd/olpx +go build -o ocswitch ./cmd/ocswitch ``` 如果你只想临时运行,也可以直接: ```bash -go run ./cmd/olpx --help +go run ./cmd/ocswitch --help ``` ## 5 分钟快速上手 ### 1. 添加上游 provider -`olpx` 要求上游是 OpenAI 兼容接口,并且 `--base-url` 需要带上 `/v1`。 +`ocswitch` 要求上游是 OpenAI 兼容接口,并且 `--base-url` 需要带上 `/v1`。 ```bash -olpx provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-xxx -olpx provider add --id codex --base-url https://api-vip.codex-for.me/v1 --api-key sk-yyy +ocswitch provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-xxx +ocswitch provider add --id codex --base-url https://api-vip.codex-for.me/v1 --api-key sk-yyy ``` 如果某个上游还需要额外请求头,可以重复传 `--header`: ```bash -olpx provider add \ +ocswitch provider add \ --id relay \ --base-url https://example.com/v1 \ --api-key sk-zzz \ @@ -79,42 +79,42 @@ olpx provider add \ 查看当前 provider: ```bash -olpx provider list +ocswitch provider list ``` ### 2. 创建 alias,并绑定多个上游 target -下面这个例子表示:当你使用 `olpx/gpt-5.4` 时,优先走 `su8/gpt-5.4`,失败后再走 `codex/GPT-5.4`。 +下面这个例子表示:当你使用 `ocswitch/gpt-5.4` 时,优先走 `su8/gpt-5.4`,失败后再走 `codex/GPT-5.4`。 ```bash -olpx alias add --name gpt-5.4 --display-name "GPT 5.4" -olpx alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 -olpx alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 +ocswitch alias add --name gpt-5.4 --display-name "GPT 5.4" +ocswitch alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 +ocswitch alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 ``` 查看当前 alias: ```bash -olpx alias list +ocswitch alias list ``` 注意: - target 的顺序就是失败切换顺序 - enabled alias 必须至少有一个可路由 target -- `olpx alias bind` 在 alias 不存在时会自动创建一个 enabled alias +- `ocswitch alias bind` 在 alias 不存在时会自动创建一个 enabled alias ### 3. 先做一次静态检查 ```bash -olpx doctor +ocswitch doctor ``` -`olpx doctor` 只做静态校验,不会真的请求上游,不会消耗额度。 +`ocswitch doctor` 只做静态校验,不会真的请求上游,不会消耗额度。 它会检查: -- 本地 `olpx` 配置能不能正常加载 +- 本地 `ocswitch` 配置能不能正常加载 - alias 是否引用了不存在的 provider - enabled alias 是否至少有一个可路由 target - 本地代理监听地址是否合理 @@ -123,56 +123,56 @@ olpx doctor ### 4. 把 alias 同步到 OpenCode ```bash -olpx opencode sync +ocswitch opencode sync ``` -这个命令会做一件事:把当前可路由的 alias 列表同步进 OpenCode 的 `provider.olpx.models`。 +这个命令会做一件事:把当前可路由的 alias 列表同步进 OpenCode 的 `provider.ocswitch.models`。 默认行为: - 优先复用全局 OpenCode 配置文件:`opencode.jsonc` > `opencode.json` > `config.json` - 如果都不存在,就创建 `~/.config/opencode/opencode.jsonc` - 默认目标明确只看全局用户配置目录,不跟随 `OPENCODE_CONFIG_DIR` -- 只更新 `provider.olpx` +- 只更新 `provider.ocswitch` - 不会修改顶层 `model` - 不会修改顶层 `small_model` -如果你希望顺手把默认模型也切到 `olpx`,需要显式指定: +如果你希望顺手把默认模型也切到 `ocswitch`,需要显式指定: ```bash -olpx opencode sync --set-model olpx/gpt-5.4 +ocswitch opencode sync --set-model ocswitch/gpt-5.4 ``` 如果你还有小模型 alias,也可以这样: ```bash -olpx opencode sync \ - --set-model olpx/gpt-5.4 \ - --set-small-model olpx/gpt-5.4-mini +ocswitch opencode sync \ + --set-model ocswitch/gpt-5.4 \ + --set-small-model ocswitch/gpt-5.4-mini ``` 先预览不写入: ```bash -olpx opencode sync --dry-run +ocswitch opencode sync --dry-run ``` 写到指定 OpenCode 配置文件: ```bash -olpx opencode sync --target /path/to/opencode.jsonc +ocswitch opencode sync --target /path/to/opencode.jsonc ``` ### 5. 启动本地代理 ```bash -olpx serve +ocswitch serve ``` 默认监听地址: - `127.0.0.1:9982` -- 本地 API Key:`olpx-local` +- 本地 API Key:`ocswitch-local` 启动后,本地代理地址是: @@ -182,47 +182,47 @@ http://127.0.0.1:9982/v1 ### 6. 在 OpenCode 里使用 -完成 `olpx opencode sync` 后,你应该能在 OpenCode 里看到 `olpx/`。 +完成 `ocswitch opencode sync` 后,你应该能在 OpenCode 里看到 `ocswitch/`。 例如: -- `olpx/gpt-5.4` +- `ocswitch/gpt-5.4` 如果你执行了: ```bash -olpx opencode sync --set-model olpx/gpt-5.4 +ocswitch opencode sync --set-model ocswitch/gpt-5.4 ``` 那么 OpenCode 默认模型也会直接切到这个 alias。 ## 直接验证本地代理 -如果你想先不走 OpenCode,直接验证 `olpx` 是否能正常代理,可以自己发一个请求: +如果你想先不走 OpenCode,直接验证 `ocswitch` 是否能正常代理,可以自己发一个请求: ```bash curl -sN -X POST http://127.0.0.1:9982/v1/responses \ - -H "Authorization: Bearer olpx-local" \ + -H "Authorization: Bearer ocswitch-local" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-5.4","stream":true,"input":"hello"}' ``` -注意这里请求体里的 `model` 是 alias 本身,例如 `gpt-5.4`,不是 `olpx/gpt-5.4`。 +注意这里请求体里的 `model` 是 alias 本身,例如 `gpt-5.4`,不是 `ocswitch/gpt-5.4`。 -因为 `olpx/gpt-5.4` 是 OpenCode 侧的模型选择写法;真正发到本地 provider 的请求里,模型名会是 alias 自身。 +因为 `ocswitch/gpt-5.4` 是 OpenCode 侧的模型选择写法;真正发到本地 provider 的请求里,模型名会是 alias 自身。 ## 从现有 OpenCode 配置导入 provider 如果你原来已经在 OpenCode 里配置过一些自定义 provider,可以直接导入: ```bash -olpx provider import-opencode +ocswitch provider import-opencode ``` 或者指定导入源: ```bash -olpx provider import-opencode --from ./examples/opencode.jsonc +ocswitch provider import-opencode --from ./examples/opencode.jsonc ``` 支持范围只有这一类: @@ -237,13 +237,13 @@ olpx provider import-opencode --from ./examples/opencode.jsonc - 默认导入源也只看全局用户配置目录,不跟随 `OPENCODE_CONFIG_DIR` - 如果你要导入别的 OpenCode 配置文件,请显式传 `--from` - 当前只导入 provider 的基本连接信息 -- 如果你的旧配置依赖额外自定义 header,需要导入后自己用 `olpx provider add --header ...` 补齐 -- `olpx` 自己不会被反向导入 +- 如果你的旧配置依赖额外自定义 header,需要导入后自己用 `ocswitch provider add --header ...` 补齐 +- `ocswitch` 自己不会被反向导入 覆盖已存在的 provider: ```bash -olpx provider import-opencode --overwrite +ocswitch provider import-opencode --overwrite ``` ## 常用命令 @@ -253,65 +253,65 @@ olpx provider import-opencode --overwrite 添加或更新 provider: ```bash -olpx provider add --id --base-url --api-key +ocswitch provider add --id --base-url --api-key ``` 查看 provider: ```bash -olpx provider list +ocswitch provider list ``` 禁用 provider: ```bash -olpx provider disable +ocswitch provider disable ``` 重新启用 provider: ```bash -olpx provider enable +ocswitch provider enable ``` 删除 provider: ```bash -olpx provider remove +ocswitch provider remove ``` -注意:删除 provider 不会自动帮你清理 alias 里的引用。引用还在的话,`olpx doctor` 会报错。 +注意:删除 provider 不会自动帮你清理 alias 里的引用。引用还在的话,`ocswitch doctor` 会报错。 ### alias 创建或更新 alias: ```bash -olpx alias add --name +ocswitch alias add --name ``` 给 alias 追加一个 target: ```bash -olpx alias bind --alias --provider --model +ocswitch alias bind --alias --provider --model ``` 解绑 target: ```bash -olpx alias unbind --alias --provider --model +ocswitch alias unbind --alias --provider --model ``` 查看 alias: ```bash -olpx alias list +ocswitch alias list ``` 删除 alias: ```bash -olpx alias remove +ocswitch alias remove ``` ### 其他 @@ -319,42 +319,42 @@ olpx alias remove 静态检查: ```bash -olpx doctor +ocswitch doctor ``` 启动代理: ```bash -olpx serve +ocswitch serve ``` 同步到 OpenCode: ```bash -olpx opencode sync +ocswitch opencode sync ``` 全局帮助: ```bash -olpx --help -olpx provider --help -olpx alias --help -olpx opencode sync --help +ocswitch --help +ocswitch provider --help +ocswitch alias --help +ocswitch opencode sync --help ``` ## 配置文件说明 -本地 `olpx` 配置文件默认路径: +本地 `ocswitch` 配置文件默认路径: -- 如果设置了 `OLPX_CONFIG`,优先使用它 -- 否则使用 `$XDG_CONFIG_HOME/olpx/config.json` -- 再否则使用 `~/.config/olpx/config.json` +- 如果设置了 `OCSWITCH_CONFIG`,优先使用它 +- 否则使用 `$XDG_CONFIG_HOME/ocswitch/config.json` +- 再否则使用 `~/.config/ocswitch/config.json` 也可以对每个命令显式指定: ```bash -olpx --config /path/to/config.json doctor +ocswitch --config /path/to/config.json doctor ``` 命令级行为、默认值、写入范围与副作用,以对应命令的 `--help` 为准;README 主要保留快速上手与背景说明。 @@ -366,7 +366,7 @@ olpx --config /path/to/config.json doctor "server": { "host": "127.0.0.1", "port": 9982, - "api_key": "olpx-local" + "api_key": "ocswitch-local" }, "providers": [ { @@ -407,7 +407,7 @@ olpx --config /path/to/config.json doctor ## 失败切换规则 -`olpx` 的切换规则很保守,也很容易理解。 +`ocswitch` 的切换规则很保守,也很容易理解。 会切换到下一个 target 的情况: @@ -438,11 +438,11 @@ olpx --config /path/to/config.json doctor 每次成功代理或透传上游错误时,响应里都会附带这些头: -- `X-OLPX-Alias` -- `X-OLPX-Provider` -- `X-OLPX-Remote-Model` -- `X-OLPX-Attempt` -- `X-OLPX-Failover-Count` +- `X-OCSWITCH-Alias` +- `X-OCSWITCH-Provider` +- `X-OCSWITCH-Remote-Model` +- `X-OCSWITCH-Attempt` +- `X-OCSWITCH-Failover-Count` 你可以用它们确认: @@ -454,18 +454,18 @@ olpx --config /path/to/config.json doctor ## 常见问题 -### 为什么 `opencode models` 里看不到 `olpx/`? +### 为什么 `opencode models` 里看不到 `ocswitch/`? 先检查这几件事: -1. 你是否执行过 `olpx opencode sync` +1. 你是否执行过 `ocswitch opencode sync` 2. 你的 alias 是否是 enabled 状态 3. alias 是否至少绑定了一个可路由 target 4. alias 绑定的 provider 是否都被禁用了 -5. OpenCode 当前实际使用的配置文件,是否就是 `olpx opencode sync` 写入的那个文件 -6. 执行一次 `olpx doctor`,看输出里的 `opencode config target` +5. OpenCode 当前实际使用的配置文件,是否就是 `ocswitch opencode sync` 写入的那个文件 +6. 执行一次 `ocswitch doctor`,看输出里的 `opencode config target` -### 为什么 `olpx doctor` 报 alias 没有可用 target? +### 为什么 `ocswitch doctor` 报 alias 没有可用 target? 因为当前实现要求:只要 alias 是 enabled,就必须至少有一个可路由的 target。 @@ -484,14 +484,14 @@ olpx --config /path/to/config.json doctor 因为 provider 可能被多个 alias 复用。 -`olpx provider disable` 只会让路由层在 failover 时自动跳过这个 provider,不会改写 alias 里的 target 状态,这样重新启用 provider 时不会和 alias 上原有的启用关系打架。 +`ocswitch provider disable` 只会让路由层在 failover 时自动跳过这个 provider,不会改写 alias 里的 target 状态,这样重新启用 provider 时不会和 alias 上原有的启用关系打架。 ### 删除 provider 后为什么还有报错? 因为 alias 里的 target 还是旧引用。需要继续执行: ```bash -olpx alias unbind --alias --provider --model +ocswitch alias unbind --alias --provider --model ``` ### 本地代理鉴权是什么? @@ -499,15 +499,15 @@ olpx alias unbind --alias --provider --model 默认是静态 key: ```text -olpx-local +ocswitch-local ``` -OpenCode 在 `provider.olpx.options.apiKey` 里会使用这个值。直接手工请求本地代理时,也要带上这个 key。 +OpenCode 在 `provider.ocswitch.options.apiKey` 里会使用这个值。直接手工请求本地代理时,也要带上这个 key。 ## 安全说明 - 默认只监听 `127.0.0.1` -- 上游凭据保存在本地 `olpx` 配置文件中 +- 上游凭据保存在本地 `ocswitch` 配置文件中 - 本项目当前没有做多用户或远程网络安全保证 所以请把本地配置文件当成敏感文件处理。 diff --git a/README_EN.md b/README_EN.md index 76fce05..9a203bc 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,15 +1,15 @@ -# opencode-provider-switch (`olpx`) +# opencode-provider-switch (`ocswitch`) A tiny local proxy for [OpenCode](https://opencode.ai) that gives you **one stable model alias** routed to **multiple upstream providers** with **deterministic failover**. -- Expose one custom provider `olpx` to OpenCode. -- Configure logical aliases (`olpx/gpt-5.4`, etc.). +- Expose one custom provider `ocswitch` to OpenCode. +- Configure logical aliases (`ocswitch/gpt-5.4`, etc.). - Each alias has an ordered list of upstream `provider/model` targets. - Providers can be disabled without mutating alias target state. - When the primary upstream returns `5xx`/`429`/connect error *before* any - stream bytes are flushed, `olpx` transparently retries the next target. + stream bytes are flushed, `ocswitch` transparently retries the next target. - Once a stream has started, the upstream is locked for the rest of that request — no mid-stream splicing. @@ -18,38 +18,38 @@ Protocol: OpenAI Responses (`POST /v1/responses`) only. Streaming supported. ## Install ```bash -go build -o olpx ./cmd/olpx +go build -o ocswitch ./cmd/ocswitch ``` ## Quick start ```bash # 1. add upstream providers -olpx provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-... -olpx provider add --id codex --base-url https://api-vip.codex-for.me/v1 --api-key sk-... +ocswitch provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-... +ocswitch provider add --id codex --base-url https://api-vip.codex-for.me/v1 --api-key sk-... # 2. create alias and bind targets in priority order -olpx alias add --name gpt-5.4 -olpx alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 -olpx alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 +ocswitch alias add --name gpt-5.4 +ocswitch alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 +ocswitch alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 # 3. push alias exposure into OpenCode global config -olpx opencode sync +ocswitch opencode sync # optional: temporarily disable one provider without editing alias targets -olpx provider disable su8 +ocswitch provider disable su8 # 4. run the proxy -olpx serve +ocswitch serve ``` -Inside OpenCode you can now pick `olpx/gpt-5.4`. +Inside OpenCode you can now pick `ocswitch/gpt-5.4`. ### Import providers from an existing OpenCode config ```bash -olpx provider import-opencode # reads global OpenCode config -olpx provider import-opencode --from ./examples/opencode.jsonc +ocswitch provider import-opencode # reads global OpenCode config +ocswitch provider import-opencode --from ./examples/opencode.jsonc ``` The default import/sync target is the global user config only. It does not @@ -62,7 +62,7 @@ imported. Everything else is out of MVP scope. ### Doctor (static) ```bash -olpx doctor +ocswitch doctor ``` Runs structural checks only — never issues real upstream requests. @@ -75,14 +75,14 @@ considered routable only when: - the referenced provider exists - the referenced provider is not disabled -`olpx opencode sync` and `/v1/models` use the same routable-alias view, so +`ocswitch opencode sync` and `/v1/models` use the same routable-alias view, so OpenCode does not see aliases that the proxy would immediately reject. ### Provider state ```bash -olpx provider disable -olpx provider enable +ocswitch provider disable +ocswitch provider enable ``` Disabling a provider only removes it from routing/failover consideration. It @@ -95,23 +95,23 @@ For exact command behavior, defaults, write scope, and side effects, prefer the matching `--help` page. This README is the quick-start narrative, while CLI help is the authoritative local execution contract. -- `olpx serve` — run the proxy -- `olpx doctor` — validate config -- `olpx provider {add,list,enable,disable,remove,import-opencode}` -- `olpx alias {add,list,bind,unbind,remove}` -- `olpx opencode sync [--target FILE] [--set-model ALIAS] [--set-small-model ALIAS] [--dry-run]` +- `ocswitch serve` — run the proxy +- `ocswitch doctor` — validate config +- `ocswitch provider {add,list,enable,disable,remove,import-opencode}` +- `ocswitch alias {add,list,bind,unbind,remove}` +- `ocswitch opencode sync [--target FILE] [--set-model ALIAS] [--set-small-model ALIAS] [--dry-run]` -Global flag: `--config PATH` (default `$XDG_CONFIG_HOME/olpx/config.json`). +Global flag: `--config PATH` (default `$OCSWITCH_CONFIG`, else `$XDG_CONFIG_HOME/ocswitch/config.json`). ## Debug headers Every proxied response includes: -- `X-OLPX-Alias` -- `X-OLPX-Provider` -- `X-OLPX-Remote-Model` -- `X-OLPX-Attempt` -- `X-OLPX-Failover-Count` +- `X-OCSWITCH-Alias` +- `X-OCSWITCH-Provider` +- `X-OCSWITCH-Remote-Model` +- `X-OCSWITCH-Attempt` +- `X-OCSWITCH-Failover-Count` ## Scope diff --git a/cmd/olpx/main.go b/cmd/ocswitch/main.go similarity index 71% rename from cmd/olpx/main.go rename to cmd/ocswitch/main.go index 0dc7eac..8686239 100644 --- a/cmd/olpx/main.go +++ b/cmd/ocswitch/main.go @@ -1,11 +1,11 @@ -// Command olpx: local alias + failover proxy for OpenCode. +// Command ocswitch: local alias + failover proxy for OpenCode. package main import ( "fmt" "os" - "github.com/anomalyco/opencode-provider-switch/internal/cli" + "github.com/Apale7/opencode-provider-switch/internal/cli" ) // version is overridden at build time via -ldflags "-X main.version=...". diff --git a/examples/opencode.jsonc b/examples/opencode.jsonc index 7164477..20d3075 100644 --- a/examples/opencode.jsonc +++ b/examples/opencode.jsonc @@ -1,5 +1,5 @@ { - // Sanitized example for `olpx provider import-opencode --from ./examples/opencode.jsonc` + // Sanitized example for `ocswitch provider import-opencode --from ./examples/opencode.jsonc` "model": "su8/gpt-5.4", "small_model": "codex/GPT-5.4-mini", "provider": { diff --git a/examples/opencode_olpx.jsonc b/examples/opencode_ocswitch.jsonc similarity index 81% rename from examples/opencode_olpx.jsonc rename to examples/opencode_ocswitch.jsonc index 98112d0..11e4c67 100644 --- a/examples/opencode_olpx.jsonc +++ b/examples/opencode_ocswitch.jsonc @@ -1,11 +1,11 @@ { - // provider.olpx.models entries may include OpenCode-only metadata. - // olpx sync preserves same-name model objects and only manages the alias set. + // provider.ocswitch.models entries may include OpenCode-only metadata. + // ocswitch sync preserves same-name model objects and only manages the alias set. "$schema": "https://opencode.ai/config.json", - "model": "olpx/gpt-5.4", - "small_model": "olpx/gpt-5.4-mini", + "model": "ocswitch/gpt-5.4", + "small_model": "ocswitch/gpt-5.4-mini", "provider": { - "olpx": { + "ocswitch": { "models": { "gpt-5.4": { "name": "gpt-5.4", @@ -50,10 +50,10 @@ "name": "gpt-5.4-mini" } }, - "name": "OpenCode LocalProxy CLI", + "name": "OpenCode Provider Switch CLI", "npm": "@ai-sdk/openai", "options": { - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "baseURL": "http://127.0.0.1:9982/v1", "setCacheKey": true } diff --git a/go.mod b/go.mod index da80ada..52a97e1 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/anomalyco/opencode-provider-switch +module github.com/Apale7/opencode-provider-switch go 1.22.2 diff --git a/internal/cli/alias.go b/internal/cli/alias.go index f5ce428..15d5477 100644 --- a/internal/cli/alias.go +++ b/internal/cli/alias.go @@ -7,26 +7,26 @@ import ( "github.com/spf13/cobra" - "github.com/anomalyco/opencode-provider-switch/internal/config" + "github.com/Apale7/opencode-provider-switch/internal/config" ) func newAliasCmd() *cobra.Command { c := &cobra.Command{ Use: "alias", - Short: "Manage logical aliases routed by olpx", + Short: "Manage logical aliases routed by ocswitch", Long: `Alias commands manage the user-facing model names that OpenCode sees as -olpx/. +ocswitch/. Each alias contains an ordered target chain of provider/model pairs. Target -order is operational: olpx tries targets in order and only fails over before any +order is operational: ocswitch tries targets in order and only fails over before any response bytes are sent downstream. Common workflow: create an alias, bind primary and fallback targets, inspect the result with alias list, then run doctor and opencode sync.`, - Example: ` olpx alias add --name gpt-5.4 --display-name "GPT 5.4" - olpx alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 - olpx alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 - olpx alias list`, + Example: ` ocswitch alias add --name gpt-5.4 --display-name "GPT 5.4" + ocswitch alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 + ocswitch alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 + ocswitch alias list`, } c.AddCommand(newAliasAddCmd(), newAliasListCmd(), newAliasBindCmd(), newAliasUnbindCmd(), newAliasRemoveCmd()) return c @@ -38,7 +38,7 @@ func newAliasAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "add", Short: "Create or update an alias (without targets)", - Long: `alias add creates or updates alias metadata in local olpx config. + Long: `alias add creates or updates alias metadata in local ocswitch config. It writes the alias record itself, but it does not add or validate targets. Enabled aliases still need at least one routable target before doctor and @@ -46,10 +46,10 @@ opencode sync will treat them as usable. When updating an existing alias, omitted display-name preserves the current value and existing targets stay attached. Typical next step: add targets with -olpx alias bind.`, - Example: ` olpx alias add --name gpt-5.4 --display-name "GPT 5.4" - olpx alias add --name gpt-5.4-mini --disabled - olpx alias add --name gpt-5.4 --display-name "GPT 5.4 Reasoning"`, +ocswitch alias bind.`, + Example: ` ocswitch alias add --name gpt-5.4 --display-name "GPT 5.4" + ocswitch alias add --name gpt-5.4-mini --disabled + ocswitch alias add --name gpt-5.4 --display-name "GPT 5.4 Reasoning"`, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return fmt.Errorf("--name is required") @@ -78,7 +78,7 @@ olpx alias bind.`, return nil }, } - cmd.Flags().StringVar(&name, "name", "", "alias name exposed as olpx/ in OpenCode (required)") + cmd.Flags().StringVar(&name, "name", "", "alias name exposed as ocswitch/ in OpenCode (required)") cmd.Flags().StringVar(&display, "display-name", "", "human-friendly display name") cmd.Flags().BoolVar(&disabled, "disabled", false, "create in disabled state") return cmd @@ -88,7 +88,7 @@ func newAliasListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List aliases and their target chains", - Long: `alias list prints aliases from local olpx config together with their target + Long: `alias list prints aliases from local ocswitch config together with their target chains. Output shows alias enabled state, target order, target enabled markers, and a @@ -96,8 +96,8 @@ note when a referenced provider is missing or disabled. This is the easiest way to verify failover order before running doctor or opencode sync. This command does not modify config and does not contact upstream providers.`, - Example: ` olpx alias list - olpx --config /path/to/config.json alias list`, + Example: ` ocswitch alias list + ocswitch --config /path/to/config.json alias list`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { @@ -143,7 +143,7 @@ func newAliasBindCmd() *cobra.Command { Use: "bind", Short: "Append a target (provider/model) to an alias in failover order", Long: `alias bind appends one provider/model target to an alias's ordered failover -chain in local olpx config. +chain in local ocswitch config. The provider must already exist. If the alias does not exist yet, this command auto-creates an enabled alias for convenience. Binding does not test upstream @@ -151,9 +151,9 @@ health or credentials. Order matters: the first bound target is tried first, the second is fallback, and so on. Typical next step: inspect with alias list, then run doctor.`, - Example: ` olpx alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 - olpx alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 - olpx alias bind --alias gpt-5.4 --provider relay --model gpt-5.4 --disabled`, + Example: ` ocswitch alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 + ocswitch alias bind --alias gpt-5.4 --provider codex --model GPT-5.4 + ocswitch alias bind --alias gpt-5.4 --provider relay --model gpt-5.4 --disabled`, RunE: func(cmd *cobra.Command, args []string) error { if alias == "" || provider == "" || model == "" { return fmt.Errorf("--alias, --provider and --model are required") @@ -192,7 +192,7 @@ func newAliasUnbindCmd() *cobra.Command { Use: "unbind", Short: "Remove a target from an alias", Long: `alias unbind removes one concrete provider/model target tuple from an alias in -local olpx config. +local ocswitch config. It does not delete the alias itself. Removing a target can leave the alias with no routable targets, which doctor and opencode sync will then treat as invalid @@ -200,8 +200,8 @@ or unavailable. Typical next step: run alias list or doctor to confirm the remaining target chain.`, - Example: ` olpx alias unbind --alias gpt-5.4 --provider codex --model GPT-5.4 - olpx doctor`, + Example: ` ocswitch alias unbind --alias gpt-5.4 --provider codex --model GPT-5.4 + ocswitch doctor`, RunE: func(cmd *cobra.Command, args []string) error { if alias == "" || provider == "" || model == "" { return fmt.Errorf("--alias, --provider and --model are required") @@ -231,16 +231,16 @@ func newAliasRemoveCmd() *cobra.Command { Use: "remove ", Args: cobra.ExactArgs(1), Short: "Delete an alias entirely", - Long: `alias remove deletes one alias and all of its target bindings from local olpx + Long: `alias remove deletes one alias and all of its target bindings from local ocswitch config. -Future opencode sync runs will stop exposing that alias in provider.olpx.models. +Future opencode sync runs will stop exposing that alias in provider.ocswitch.models. This command does not directly clear top-level model selections that may still reference the old alias in OpenCode config. -Typical next step: run olpx opencode sync if OpenCode exposure should be updated.`, - Example: ` olpx alias remove gpt-5.4 - olpx opencode sync`, +Typical next step: run ocswitch opencode sync if OpenCode exposure should be updated.`, + Example: ` ocswitch alias remove gpt-5.4 + ocswitch opencode sync`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 9a76bc6..868df99 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -9,11 +9,11 @@ import ( "github.com/spf13/cobra" - "github.com/anomalyco/opencode-provider-switch/internal/config" + "github.com/Apale7/opencode-provider-switch/internal/config" ) func TestProviderAddPreservesExistingFields(t *testing.T) { - t.Setenv("OLPX_CONFIG", filepath.Join(t.TempDir(), "olpx.json")) + t.Setenv(config.ConfigEnvVar, filepath.Join(t.TempDir(), "ocswitch.json")) configPath = "" cfg, err := loadCfg() @@ -64,8 +64,8 @@ func TestProviderAddPreservesExistingFields(t *testing.T) { } func TestProviderAddRejectsInvalidBaseURL(t *testing.T) { - configFile := filepath.Join(t.TempDir(), "olpx.json") - t.Setenv("OLPX_CONFIG", configFile) + configFile := filepath.Join(t.TempDir(), "ocswitch.json") + t.Setenv(config.ConfigEnvVar, configFile) configPath = "" cmd := newProviderAddCmd() @@ -83,7 +83,7 @@ func TestProviderAddRejectsInvalidBaseURL(t *testing.T) { } func TestAliasAddPreservesExistingFields(t *testing.T) { - t.Setenv("OLPX_CONFIG", filepath.Join(t.TempDir(), "olpx.json")) + t.Setenv(config.ConfigEnvVar, filepath.Join(t.TempDir(), "ocswitch.json")) configPath = "" cfg, err := loadCfg() @@ -126,7 +126,7 @@ func TestAliasAddPreservesExistingFields(t *testing.T) { } func TestProviderEnableDisableCommands(t *testing.T) { - t.Setenv("OLPX_CONFIG", filepath.Join(t.TempDir(), "olpx.json")) + t.Setenv(config.ConfigEnvVar, filepath.Join(t.TempDir(), "ocswitch.json")) configPath = "" cfg, err := loadCfg() @@ -171,7 +171,7 @@ func TestProviderEnableDisableCommands(t *testing.T) { } func TestOpencodeSyncDoesNotPanicOnSliceModelMetadata(t *testing.T) { - t.Setenv("OLPX_CONFIG", filepath.Join(t.TempDir(), "olpx.json")) + t.Setenv(config.ConfigEnvVar, filepath.Join(t.TempDir(), "ocswitch.json")) configPath = "" cfg, err := loadCfg() @@ -192,7 +192,7 @@ func TestOpencodeSyncDoesNotPanicOnSliceModelMetadata(t *testing.T) { } target := filepath.Join(t.TempDir(), "opencode.jsonc") - seed := []byte("{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"provider\": {\n \"olpx\": {\n \"npm\": \"@ai-sdk/openai\",\n \"name\": \"OpenCode LocalProxy CLI\",\n \"options\": {\n \"baseURL\": \"http://127.0.0.1:9982/v1\",\n \"apiKey\": \"olpx-local\",\n \"setCacheKey\": true\n },\n \"models\": {\n \"gpt-5.4\": {\n \"name\": \"custom-display-name\",\n \"tags\": [\"reasoning\", \"priority\"],\n \"variants\": [\n {\"name\": \"high\", \"effort\": \"high\"}\n ]\n }\n }\n }\n }\n}\n") + seed := []byte("{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"provider\": {\n \"ocswitch\": {\n \"npm\": \"@ai-sdk/openai\",\n \"name\": \"OpenCode Provider Switch CLI\",\n \"options\": {\n \"baseURL\": \"http://127.0.0.1:9982/v1\",\n \"apiKey\": \"ocswitch-local\",\n \"setCacheKey\": true\n },\n \"models\": {\n \"gpt-5.4\": {\n \"name\": \"custom-display-name\",\n \"tags\": [\"reasoning\", \"priority\"],\n \"variants\": [\n {\"name\": \"high\", \"effort\": \"high\"}\n ]\n }\n }\n }\n }\n}\n") if err := os.WriteFile(target, seed, 0o600); err != nil { t.Fatalf("write target config: %v", err) } @@ -235,13 +235,13 @@ func TestHelpTextIncludesOperationalGuidance(t *testing.T) { name: "root", cmd: NewRootCmd("test"), wantLong: []string{"Typical workflow:", "prefer command-local --help over README summaries"}, - wantExample: []string{"olpx provider add", "olpx serve"}, + wantExample: []string{"ocswitch provider add", "ocswitch serve"}, }, { name: "provider add", cmd: newProviderAddCmd(), wantLong: []string{"--base-url must point at an", "omitted mutable fields keep their current", "values: name, api key, headers"}, - wantExample: []string{"olpx provider add --id relay", "--header X-Workspace=my-team"}, + wantExample: []string{"ocswitch provider add --id relay", "--header X-Workspace=my-team"}, }, { name: "alias bind", @@ -252,8 +252,8 @@ func TestHelpTextIncludesOperationalGuidance(t *testing.T) { { name: "opencode sync", cmd: newOpencodeSyncCmd(), - wantLong: []string{"does not follow", "writes alias", "exposure into provider.olpx.models"}, - wantExample: []string{"--dry-run", "--set-small-model olpx/gpt-5.4-mini"}, + wantLong: []string{"does not follow", "writes alias", "exposure into provider.ocswitch.models"}, + wantExample: []string{"--dry-run", "--set-small-model ocswitch/gpt-5.4-mini"}, }, } @@ -283,7 +283,7 @@ func TestHelpTextIncludesOperationalGuidance(t *testing.T) { if flag == nil { t.Fatal("--config flag not found") } - for _, want := range []string{"$OLPX_CONFIG", "$XDG_CONFIG_HOME/olpx/config.json", "~/.config/olpx/config.json"} { + for _, want := range []string{"$OCSWITCH_CONFIG", "$XDG_CONFIG_HOME/ocswitch/config.json", "~/.config/ocswitch/config.json"} { if !strings.Contains(flag.Usage, want) { t.Fatalf("config flag usage missing %q: %s", want, flag.Usage) } diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 7f3adc0..099b955 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -5,25 +5,25 @@ import ( "github.com/spf13/cobra" - "github.com/anomalyco/opencode-provider-switch/internal/opencode" + "github.com/Apale7/opencode-provider-switch/internal/opencode" ) func newDoctorCmd() *cobra.Command { return &cobra.Command{ Use: "doctor", - Short: "Validate olpx config (static checks, no upstream requests)", - Long: `doctor performs static validation for local olpx config and the expected + Short: "Validate ocswitch config (static checks, no upstream requests)", + Long: `doctor performs static validation for local ocswitch config and the expected OpenCode sync result. -It loads local olpx config, checks alias/provider consistency, resolves the -default OpenCode sync target, and validates what provider.olpx would look like +It loads local ocswitch config, checks alias/provider consistency, resolves the + default OpenCode sync target, and validates what provider.ocswitch would look like there. It does not send any real requests to upstream providers and does not consume model quota. Run doctor before opencode sync or serve whenever you changed providers, aliases, or local server settings.`, - Example: ` olpx doctor - olpx --config /path/to/config.json doctor`, + Example: ` ocswitch doctor + ocswitch --config /path/to/config.json doctor`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { @@ -38,9 +38,9 @@ aliases, or local server settings.`, } else { aliasNames := cfg.AvailableAliasNames() baseURL := fmt.Sprintf("http://%s:%d/v1", cfg.Server.Host, cfg.Server.Port) - opencode.EnsureOLPXProvider(raw, baseURL, cfg.Server.APIKey, aliasNames) - if err := opencode.ValidateOLPXProvider(raw, baseURL, cfg.Server.APIKey, aliasNames); err != nil { - issues = append(issues, fmt.Errorf("opencode provider.olpx invalid: %w", err)) + opencode.EnsureOcswitchProvider(raw, baseURL, cfg.Server.APIKey, aliasNames) + if err := opencode.ValidateOcswitchProvider(raw, baseURL, cfg.Server.APIKey, aliasNames); err != nil { + issues = append(issues, fmt.Errorf("opencode provider.ocswitch invalid: %w", err)) } } ok := len(issues) == 0 @@ -61,7 +61,7 @@ aliases, or local server settings.`, marker = "(exists)" } fmt.Fprintf(cmd.OutOrStdout(), " opencode config target: %s %s\n", path, marker) - fmt.Fprintf(cmd.OutOrStdout(), " provider.olpx preview: valid=%v\n", ok) + fmt.Fprintf(cmd.OutOrStdout(), " provider.ocswitch preview: valid=%v\n", ok) fmt.Fprintf(cmd.OutOrStdout(), " proxy bind: %s:%d\n", cfg.Server.Host, cfg.Server.Port) if !ok { diff --git a/internal/cli/opencode.go b/internal/cli/opencode.go index fded874..8d55e87 100644 --- a/internal/cli/opencode.go +++ b/internal/cli/opencode.go @@ -5,24 +5,24 @@ import ( "github.com/spf13/cobra" - "github.com/anomalyco/opencode-provider-switch/internal/opencode" + "github.com/Apale7/opencode-provider-switch/internal/opencode" ) func newOpencodeCmd() *cobra.Command { c := &cobra.Command{ Use: "opencode", Short: "OpenCode integration commands", - Long: `OpenCode commands manage the narrow integration boundary between olpx and + Long: `OpenCode commands manage the narrow integration boundary between ocswitch and OpenCode config. These commands do not attempt full OpenCode config takeover. They are limited to -the provider.olpx sync path and optional top-level model fields when you opt in +the provider.ocswitch sync path and optional top-level model fields when you opt in explicitly. Common workflow: validate with doctor first, inspect sync help, then run opencode sync.`, - Example: ` olpx opencode sync --dry-run - olpx opencode sync --set-model olpx/gpt-5.4`, + Example: ` ocswitch opencode sync --dry-run + ocswitch opencode sync --set-model ocswitch/gpt-5.4`, } c.AddCommand(newOpencodeSyncCmd()) return c @@ -35,8 +35,8 @@ func newOpencodeSyncCmd() *cobra.Command { var dryRun bool cmd := &cobra.Command{ Use: "sync", - Short: "Update provider.olpx in the global OpenCode config to match current aliases", - Long: `olpx opencode sync writes provider.olpx into the target OpenCode config. + Short: "Update provider.ocswitch in the global OpenCode config to match current aliases", + Long: `ocswitch opencode sync writes provider.ocswitch into the target OpenCode config. By default it targets the global user config (~/.config/opencode), picking the existing file in precedence order opencode.jsonc > opencode.json > config.json, @@ -45,17 +45,17 @@ or creating opencode.jsonc if none exists. It does NOT touch the top-level The default target scope is only the global user config path; it does not follow OPENCODE_CONFIG_DIR unless you pass --target yourself. The command writes alias -exposure into provider.olpx.models using only aliases that are currently +exposure into provider.ocswitch.models using only aliases that are currently routable. Use --dry-run to preview the resolved target file without writing it. Typical -workflow: run olpx doctor first, then sync, then start or restart olpx serve if +workflow: run ocswitch doctor first, then sync, then start or restart ocswitch serve if needed.`, - Example: ` olpx opencode sync - olpx opencode sync --dry-run - olpx opencode sync --set-model olpx/gpt-5.4 - olpx opencode sync --set-model olpx/gpt-5.4 --set-small-model olpx/gpt-5.4-mini - olpx opencode sync --target /path/to/opencode.jsonc`, + Example: ` ocswitch opencode sync + ocswitch opencode sync --dry-run + ocswitch opencode sync --set-model ocswitch/gpt-5.4 + ocswitch opencode sync --set-model ocswitch/gpt-5.4 --set-small-model ocswitch/gpt-5.4-mini + ocswitch opencode sync --target /path/to/opencode.jsonc`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { @@ -78,7 +78,7 @@ needed.`, } aliasNames := cfg.AvailableAliasNames() baseURL := fmt.Sprintf("http://%s:%d/v1", cfg.Server.Host, cfg.Server.Port) - changed := opencode.EnsureOLPXProvider(raw, baseURL, cfg.Server.APIKey, aliasNames) + changed := opencode.EnsureOcswitchProvider(raw, baseURL, cfg.Server.APIKey, aliasNames) if setModel != "" { if raw["model"] != setModel { raw["model"] = setModel @@ -102,7 +102,7 @@ needed.`, if err := opencode.Save(path, raw); err != nil { return err } - fmt.Fprintf(cmd.OutOrStdout(), "synced provider.olpx into %s (%d alias(es))\n", path, len(aliasNames)) + fmt.Fprintf(cmd.OutOrStdout(), "synced provider.ocswitch into %s (%d alias(es))\n", path, len(aliasNames)) if setModel != "" { fmt.Fprintf(cmd.OutOrStdout(), " model = %s\n", setModel) } diff --git a/internal/cli/provider.go b/internal/cli/provider.go index 9679e9c..4761284 100644 --- a/internal/cli/provider.go +++ b/internal/cli/provider.go @@ -7,8 +7,8 @@ import ( "github.com/spf13/cobra" - "github.com/anomalyco/opencode-provider-switch/internal/config" - "github.com/anomalyco/opencode-provider-switch/internal/opencode" + "github.com/Apale7/opencode-provider-switch/internal/config" + "github.com/Apale7/opencode-provider-switch/internal/opencode" ) func newProviderCmd() *cobra.Command { @@ -16,17 +16,17 @@ func newProviderCmd() *cobra.Command { Use: "provider", Short: "Manage upstream providers", Long: `Provider commands manage upstream OpenAI-compatible endpoints stored in the -local olpx config file. +local ocswitch config file. Providers are separate from aliases: a provider defines connection details such as base URL, API key, and extra headers, while aliases decide failover order by binding one or more provider/model targets. Common workflow: add or import providers first, inspect them with provider list, -then bind them to aliases with olpx alias bind.`, - Example: ` olpx provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-example - olpx provider import-opencode - olpx provider list`, +then bind them to aliases with ocswitch alias bind.`, + Example: ` ocswitch provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-example + ocswitch provider import-opencode + ocswitch provider list`, } c.AddCommand( newProviderAddCmd(), @@ -46,10 +46,10 @@ func newProviderAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "add", Short: "Add or update an upstream provider", - Long: `provider add creates or updates one upstream provider entry in local olpx + Long: `provider add creates or updates one upstream provider entry in local ocswitch config. -It writes only the olpx config file. --base-url must point at an +It writes only the ocswitch config file. --base-url must point at an OpenAI-compatible /v1 root. The command validates that shape, but it does not contact the upstream or test credentials. @@ -58,11 +58,11 @@ values: name, api key, headers, and disabled state are preserved unless you explicitly pass new values. Repeated --header KEY=VALUE entries replace the stored header map for this command invocation. -Typical next step: run olpx provider list or bind the provider to an alias.`, - Example: ` olpx provider add --id su8 --base-url https://cn2.su8.codes/v1 - olpx provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-example - olpx provider add --id relay --base-url https://example.com/v1 --api-key sk-example --header X-Token=abc --header X-Workspace=my-team - olpx provider add --id su8 --base-url https://new.example.com/v1`, +Typical next step: run ocswitch provider list or bind the provider to an alias.`, + Example: ` ocswitch provider add --id su8 --base-url https://cn2.su8.codes/v1 + ocswitch provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-example + ocswitch provider add --id relay --base-url https://example.com/v1 --api-key sk-example --header X-Token=abc --header X-Workspace=my-team + ocswitch provider add --id su8 --base-url https://new.example.com/v1`, RunE: func(cmd *cobra.Command, args []string) error { if id == "" || baseURL == "" { return fmt.Errorf("--id and --base-url are required") @@ -140,15 +140,15 @@ func newProviderListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List configured providers", - Long: `provider list prints the providers currently stored in local olpx config. + Long: `provider list prints the providers currently stored in local ocswitch config. Output is inspection-oriented: provider ids, enabled state, base URLs, and redacted API keys are shown so you can confirm what was saved or imported before binding aliases. This command does not modify config and does not contact upstream providers.`, - Example: ` olpx provider list - olpx --config /path/to/config.json provider list`, + Example: ` ocswitch provider list + ocswitch --config /path/to/config.json provider list`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { @@ -193,16 +193,16 @@ func newProviderStateCmd(use string, disabled bool) *cobra.Command { Use: use + " ", Args: cobra.ExactArgs(1), Short: strings.Title(action[:len(action)-1]) + " a provider without changing alias target state", - Long: fmt.Sprintf(`provider %s flips one provider's disabled state in local olpx config. + Long: fmt.Sprintf(`provider %s flips one provider's disabled state in local ocswitch config. It changes routing eligibility for every alias target that references this provider, but it does not rewrite alias target enabled flags. This matters when the same provider is shared across multiple aliases. -This command writes only the olpx config file and does not test upstream -reachability. Typical next step: run olpx doctor to confirm routable aliases.`, use), - Example: fmt.Sprintf(` olpx provider %s - olpx doctor`, use), +This command writes only the ocswitch config file and does not test upstream +reachability. Typical next step: run ocswitch doctor to confirm routable aliases.`, use), + Example: fmt.Sprintf(` ocswitch provider %s + ocswitch doctor`, use), RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { @@ -229,16 +229,16 @@ func newProviderRemoveCmd() *cobra.Command { Use: "remove ", Args: cobra.ExactArgs(1), Short: "Remove a provider (targets referencing it must be removed first or will fail doctor)", - Long: `provider remove deletes one provider from local olpx config. + Long: `provider remove deletes one provider from local ocswitch config. It does not automatically clean alias target references that still point at the removed provider. If aliases still reference it, doctor will report invalid config and those aliases will not be routable. -Typical follow-up: inspect aliases, unbind stale targets, then run olpx doctor.`, - Example: ` olpx provider remove su8 - olpx alias list - olpx doctor`, +Typical follow-up: inspect aliases, unbind stale targets, then run ocswitch doctor.`, + Example: ` ocswitch provider remove su8 + ocswitch alias list + ocswitch doctor`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { @@ -263,7 +263,7 @@ func newProviderImportCmd() *cobra.Command { Use: "import-opencode", Short: "Import @ai-sdk/openai custom providers from an OpenCode config file", Long: `provider import-opencode reads an OpenCode config file and copies supported -custom providers into local olpx config. +custom providers into local ocswitch config. By default it reads the global user OpenCode config resolved in precedence order opencode.jsonc > opencode.json > config.json under ~/.config/opencode (XDG @@ -271,13 +271,13 @@ aware). It does not follow OPENCODE_CONFIG_DIR for this default source; use --from when you want a different file. Only config-defined @ai-sdk/openai custom providers with both baseURL and apiKey -are imported. Unsupported provider shapes are skipped by design. Existing olpx +are imported. Unsupported provider shapes are skipped by design. Existing ocswitch providers are skipped unless --overwrite is given. -Typical next step: run olpx provider list, then create aliases and bindings.`, - Example: ` olpx provider import-opencode - olpx provider import-opencode --from /path/to/opencode.jsonc - olpx provider import-opencode --overwrite`, +Typical next step: run ocswitch provider list, then create aliases and bindings.`, + Example: ` ocswitch provider import-opencode + ocswitch provider import-opencode --from /path/to/opencode.jsonc + ocswitch provider import-opencode --overwrite`, RunE: func(cmd *cobra.Command, args []string) error { if srcPath == "" { p, existed := opencode.ResolveGlobalConfigPath() diff --git a/internal/cli/root.go b/internal/cli/root.go index a090149..d6a5281 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -1,4 +1,4 @@ -// Package cli wires the olpx cobra command tree. +// Package cli wires the ocswitch cobra command tree. package cli import ( @@ -7,50 +7,50 @@ import ( "github.com/spf13/cobra" - "github.com/anomalyco/opencode-provider-switch/internal/config" + "github.com/Apale7/opencode-provider-switch/internal/config" ) // configPath is populated from the global --config flag. var configPath string -// loadCfg opens the active olpx config, with the selected path. +// loadCfg opens the active ocswitch config, with the selected path. func loadCfg() (*config.Config, error) { return config.Load(configPath) } -// NewRootCmd builds the root olpx command. +// NewRootCmd builds the root ocswitch command. func NewRootCmd(version string) *cobra.Command { root := &cobra.Command{ - Use: "olpx", - Short: "OpenCode LocalProxy CLI: local alias + failover proxy for OpenCode", - Long: `olpx is a local OpenCode proxy that exposes stable aliases as olpx/ + Use: config.AppName, + Short: "OpenCode Provider Switch CLI: local alias + failover proxy for OpenCode", + Long: `ocswitch is a local OpenCode proxy that exposes stable aliases as ocswitch/ while routing each alias to one or more upstream provider/model targets. Typical workflow: -1. add or import upstream providers into local olpx config +1. add or import upstream providers into local ocswitch config 2. create aliases and bind ordered targets 3. run doctor for static validation -4. run opencode sync to write provider.olpx into OpenCode config +4. run opencode sync to write provider.ocswitch into OpenCode config 5. run serve to accept local /v1/responses traffic -olpx writes only its own local config unless a command explicitly says it also +ocswitch writes only its own local config unless a command explicitly says it also writes OpenCode config. For exact command behavior, defaults, and side effects, prefer command-local --help over README summaries.`, - Example: ` olpx provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-example - olpx alias add --name gpt-5.4 --display-name "GPT 5.4" - olpx alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 - olpx doctor - olpx opencode sync - olpx serve + Example: ` ocswitch provider add --id su8 --base-url https://cn2.su8.codes/v1 --api-key sk-example + ocswitch alias add --name gpt-5.4 --display-name "GPT 5.4" + ocswitch alias bind --alias gpt-5.4 --provider su8 --model gpt-5.4 + ocswitch doctor + ocswitch opencode sync + ocswitch serve - olpx provider import-opencode - olpx provider list - olpx opencode sync --dry-run`, + ocswitch provider import-opencode + ocswitch provider list + ocswitch opencode sync --dry-run`, SilenceUsage: true, SilenceErrors: false, Version: version, } - root.PersistentFlags().StringVar(&configPath, "config", "", "path to olpx config.json (default: $OLPX_CONFIG, else $XDG_CONFIG_HOME/olpx/config.json, else ~/.config/olpx/config.json)") + root.PersistentFlags().StringVar(&configPath, "config", "", fmt.Sprintf("path to %s config.json (default: $%s, else $XDG_CONFIG_HOME/%s/config.json, else ~/.config/%s/config.json)", config.AppName, config.ConfigEnvVar, config.ConfigDirName, config.ConfigDirName)) root.AddCommand(newServeCmd()) root.AddCommand(newDoctorCmd()) diff --git a/internal/cli/serve.go b/internal/cli/serve.go index 1529754..b22d3ad 100644 --- a/internal/cli/serve.go +++ b/internal/cli/serve.go @@ -6,24 +6,24 @@ import ( "github.com/spf13/cobra" - "github.com/anomalyco/opencode-provider-switch/internal/proxy" + "github.com/Apale7/opencode-provider-switch/internal/proxy" ) func newServeCmd() *cobra.Command { return &cobra.Command{ Use: "serve", - Short: "Run the local olpx proxy (alias -> failover upstream)", - Long: `serve starts the long-running local olpx proxy using the current local config. + Short: "Run the local ocswitch proxy (alias -> failover upstream)", + Long: `serve starts the long-running local ocswitch proxy using the current local config. It reads local alias/provider configuration, validates that config, and then accepts OpenAI Responses traffic at the configured local base URL. With default settings the proxy listens on http://127.0.0.1:9982/v1 and expects the local API -key olpx-local. +key ocswitch-local. serve does not rewrite config files. Run doctor and opencode sync first so OpenCode can see the same aliases that the proxy can route.`, - Example: ` olpx serve - olpx --config /path/to/config.json serve`, + Example: ` ocswitch serve + ocswitch --config /path/to/config.json serve`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadCfg() if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index c024b8a..b823bc3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,4 +1,4 @@ -// Package config manages the local olpx JSON config file. +// Package config manages the local ocswitch JSON config file. package config import ( @@ -12,6 +12,13 @@ import ( "sync" ) +const ( + AppName = "ocswitch" + ConfigEnvVar = "OCSWITCH_CONFIG" + ConfigDirName = "ocswitch" + DefaultLocalAPIKey = "ocswitch-local" +) + // Target is one concrete upstream candidate for an alias. type Target struct { Provider string `json:"provider"` @@ -44,7 +51,7 @@ type Server struct { APIKey string `json:"api_key"` } -// Config is the on-disk olpx config. +// Config is the on-disk ocswitch config. type Config struct { Server Server `json:"server"` Providers []Provider `json:"providers"` @@ -78,26 +85,26 @@ func Default() *Config { Server: Server{ Host: "127.0.0.1", Port: 9982, - APIKey: "olpx-local", + APIKey: DefaultLocalAPIKey, }, Providers: []Provider{}, Aliases: []Alias{}, } } -// DefaultPath returns ~/.config/olpx/config.json (XDG aware). +// DefaultPath returns ~/.config/ocswitch/config.json (XDG aware). func DefaultPath() string { - if p := os.Getenv("OLPX_CONFIG"); p != "" { + if p := os.Getenv(ConfigEnvVar); p != "" { return p } if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { - return filepath.Join(xdg, "olpx", "config.json") + return filepath.Join(xdg, ConfigDirName, "config.json") } home, err := os.UserHomeDir() if err != nil { - return "olpx-config.json" + return AppName + "-config.json" } - return filepath.Join(home, ".config", "olpx", "config.json") + return filepath.Join(home, ".config", ConfigDirName, "config.json") } // Load reads the config at path. Missing file returns a default config anchored to path. @@ -127,7 +134,7 @@ func Load(path string) (*Config, error) { c.Server.Port = 9982 } if c.Server.APIKey == "" { - c.Server.APIKey = "olpx-local" + c.Server.APIKey = DefaultLocalAPIKey } c.path = path return c, nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 344a878..678f1b9 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -93,7 +93,7 @@ func TestValidateReportsAliasWithoutAvailableTargets(t *testing.T) { t.Parallel() cfg := &Config{ - Server: Server{Host: "127.0.0.1", Port: 9982, APIKey: "olpx-local"}, + Server: Server{Host: "127.0.0.1", Port: 9982, APIKey: DefaultLocalAPIKey}, Providers: []Provider{{ID: "p1", BaseURL: "https://p1.example.com/v1", Disabled: true}}, Aliases: []Alias{{ Alias: "gpt-5.4", diff --git a/internal/opencode/opencode.go b/internal/opencode/opencode.go index 63b1ed1..9dc4100 100644 --- a/internal/opencode/opencode.go +++ b/internal/opencode/opencode.go @@ -1,5 +1,5 @@ // Package opencode reads and writes OpenCode config files, including the -// `provider.olpx` sync path. Files may be JSON or JSONC; we preserve the +// `provider.ocswitch` sync path. Files may be JSON or JSONC; we preserve the // detected extension on write. package opencode @@ -16,6 +16,11 @@ import ( "github.com/tidwall/jsonc" ) +const ( + ProviderKey = "ocswitch" + ProviderName = "OpenCode Provider Switch CLI" +) + // ConfigFileCandidates is the precedence order inside the global config dir. var ConfigFileCandidates = []string{"opencode.jsonc", "opencode.json", "config.json"} @@ -72,8 +77,8 @@ func Load(path string) (Raw, error) { return out, nil } -// Save writes provider.olpx back to path. Existing files are normalized to plain -// JSON and only the provider.olpx subtree is patched so unrelated key order stays +// Save writes provider.ocswitch back to path. Existing files are normalized to plain +// JSON and only the provider.ocswitch subtree is patched so unrelated key order stays // stable. New files are still written from the full Raw object. Writes are // atomic. func Save(path string, raw Raw) error { @@ -102,7 +107,7 @@ func renderSaveData(path string, raw Raw) ([]byte, error) { if len(bytes.TrimSpace(original)) == 0 { return marshalRaw(raw) } - patched, err := patchProviderOLPXDocument(original, raw) + patched, err := patchProviderDocument(original, raw) if err != nil { return nil, fmt.Errorf("patch %s: %w", path, err) } @@ -122,8 +127,8 @@ func marshalRaw(raw Raw) ([]byte, error) { return data, nil } -func patchProviderOLPXDocument(original []byte, raw Raw) ([]byte, error) { - olpxRaw, ok := olpxProviderValue(raw) +func patchProviderDocument(original []byte, raw Raw) ([]byte, error) { + providerValue, ok := syncedProviderValue(raw) if !ok { return marshalRaw(raw) } @@ -144,7 +149,7 @@ func patchProviderOLPXDocument(original []byte, raw Raw) ([]byte, error) { } provider := root.findMember("provider") if provider == nil { - return insertObjectMember(normalized, root, "provider", map[string]any{"olpx": olpxRaw}) + return insertObjectMember(normalized, root, "provider", map[string]any{ProviderKey: providerValue}) } if normalized[provider.valueStart] != '{' { return nil, fmt.Errorf("top-level provider must be an object") @@ -153,23 +158,23 @@ func patchProviderOLPXDocument(original []byte, raw Raw) ([]byte, error) { if err != nil { return nil, err } - olpx := providerObj.findMember("olpx") - if olpx == nil { - return insertObjectMember(normalized, providerObj, "olpx", olpxRaw) + providerEntry := providerObj.findMember(ProviderKey) + if providerEntry == nil { + return insertObjectMember(normalized, providerObj, ProviderKey, providerValue) } - return replaceObjectMember(normalized, *olpx, olpxRaw) + return replaceObjectMember(normalized, *providerEntry, providerValue) } -func olpxProviderValue(raw Raw) (map[string]any, bool) { +func syncedProviderValue(raw Raw) (map[string]any, bool) { providerRaw, _ := raw["provider"].(map[string]any) if providerRaw == nil { return nil, false } - olpxRaw, _ := providerRaw["olpx"].(map[string]any) - if olpxRaw == nil { + providerEntry, _ := providerRaw[ProviderKey].(map[string]any) + if providerEntry == nil { return nil, false } - return olpxRaw, true + return providerEntry, true } type objectSpan struct { @@ -410,13 +415,13 @@ func lineIndent(data []byte, pos int) string { return string(data[lineStart:lineEnd]) } -// EnsureOLPXProvider updates (or creates) the provider.olpx entry with the given -// local base URL, local api key and alias set. Existing keys on provider.olpx +// EnsureOcswitchProvider updates (or creates) the provider.ocswitch entry with the given +// local base URL, local api key and alias set. Existing keys on provider.ocswitch // are preserved unless they conflict with the sync intent. For model entries, // sync owns only the alias set: same-name model objects are left untouched so // OpenCode-only metadata survives round-trips. Returns true if the file would // actually change. -func EnsureOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) bool { +func EnsureOcswitchProvider(raw Raw, baseURL, apiKey string, aliases []string) bool { changed := false if _, ok := raw["$schema"]; !ok { raw["$schema"] = "https://opencode.ai/config.json" @@ -428,22 +433,22 @@ func EnsureOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) bool raw["provider"] = provRaw changed = true } - olpxRaw, _ := provRaw["olpx"].(map[string]any) - if olpxRaw == nil { - olpxRaw = map[string]any{} - provRaw["olpx"] = olpxRaw + providerEntry, _ := provRaw[ProviderKey].(map[string]any) + if providerEntry == nil { + providerEntry = map[string]any{} + provRaw[ProviderKey] = providerEntry changed = true } - if setIfDiff(olpxRaw, "npm", "@ai-sdk/openai") { + if setIfDiff(providerEntry, "npm", "@ai-sdk/openai") { changed = true } - if setIfDiff(olpxRaw, "name", "OpenCode LocalProxy CLI") { + if setIfDiff(providerEntry, "name", ProviderName) { changed = true } - opts, _ := olpxRaw["options"].(map[string]any) + opts, _ := providerEntry["options"].(map[string]any) if opts == nil { opts = map[string]any{} - olpxRaw["options"] = opts + providerEntry["options"] = opts changed = true } if setIfDiff(opts, "baseURL", baseURL) { @@ -457,7 +462,7 @@ func EnsureOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) bool } // Build models map from alias list. Preserve any existing per-model objects // verbatim if the alias key matches; drop aliases removed locally. - existingModels, _ := olpxRaw["models"].(map[string]any) + existingModels, _ := providerEntry["models"].(map[string]any) newModels := map[string]any{} aliasSet := map[string]bool{} for _, a := range aliases { @@ -476,43 +481,43 @@ func EnsureOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) bool } } if !mapsEqualShallow(existingModels, newModels) { - olpxRaw["models"] = newModels + providerEntry["models"] = newModels } return changed } -// ValidateOLPXProvider checks that provider.olpx matches the MVP sync contract. -func ValidateOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) error { +// ValidateOcswitchProvider checks that provider.ocswitch matches the MVP sync contract. +func ValidateOcswitchProvider(raw Raw, baseURL, apiKey string, aliases []string) error { provRaw, _ := raw["provider"].(map[string]any) if provRaw == nil { return fmt.Errorf("missing provider object") } - olpxRaw, _ := provRaw["olpx"].(map[string]any) - if olpxRaw == nil { - return fmt.Errorf("missing provider.olpx") + providerEntry, _ := provRaw[ProviderKey].(map[string]any) + if providerEntry == nil { + return fmt.Errorf("missing provider.%s", ProviderKey) } - if npm, _ := olpxRaw["npm"].(string); npm != "@ai-sdk/openai" { - return fmt.Errorf("provider.olpx.npm must be @ai-sdk/openai") + if npm, _ := providerEntry["npm"].(string); npm != "@ai-sdk/openai" { + return fmt.Errorf("provider.%s.npm must be @ai-sdk/openai", ProviderKey) } - if name, _ := olpxRaw["name"].(string); name != "OpenCode LocalProxy CLI" { - return fmt.Errorf("provider.olpx.name must be OpenCode LocalProxy CLI") + if name, _ := providerEntry["name"].(string); name != ProviderName { + return fmt.Errorf("provider.%s.name must be %s", ProviderKey, ProviderName) } - opts, _ := olpxRaw["options"].(map[string]any) + opts, _ := providerEntry["options"].(map[string]any) if opts == nil { - return fmt.Errorf("provider.olpx.options missing") + return fmt.Errorf("provider.%s.options missing", ProviderKey) } if got, _ := opts["baseURL"].(string); got != baseURL { - return fmt.Errorf("provider.olpx.options.baseURL mismatch") + return fmt.Errorf("provider.%s.options.baseURL mismatch", ProviderKey) } if got, _ := opts["apiKey"].(string); got != apiKey { - return fmt.Errorf("provider.olpx.options.apiKey mismatch") + return fmt.Errorf("provider.%s.options.apiKey mismatch", ProviderKey) } if got, ok := opts["setCacheKey"].(bool); !ok || !got { - return fmt.Errorf("provider.olpx.options.setCacheKey must be true") + return fmt.Errorf("provider.%s.options.setCacheKey must be true", ProviderKey) } - models, _ := olpxRaw["models"].(map[string]any) + models, _ := providerEntry["models"].(map[string]any) if models == nil { - return fmt.Errorf("provider.olpx.models missing") + return fmt.Errorf("provider.%s.models missing", ProviderKey) } expected := append([]string(nil), aliases...) sort.Strings(expected) @@ -520,17 +525,17 @@ func ValidateOLPXProvider(raw Raw, baseURL, apiKey string, aliases []string) err for alias, v := range models { modelCfg, _ := v.(map[string]any) if modelCfg == nil { - return fmt.Errorf("provider.olpx.models.%s must be an object", alias) + return fmt.Errorf("provider.%s.models.%s must be an object", ProviderKey, alias) } actual = append(actual, alias) } sort.Strings(actual) if len(actual) != len(expected) { - return fmt.Errorf("provider.olpx.models alias set mismatch") + return fmt.Errorf("provider.%s.models alias set mismatch", ProviderKey) } for i := range actual { if actual[i] != expected[i] { - return fmt.Errorf("provider.olpx.models alias set mismatch") + return fmt.Errorf("provider.%s.models alias set mismatch", ProviderKey) } } return nil @@ -556,13 +561,13 @@ type ImportableProvider struct { } // ImportCustomProviders scans raw for @ai-sdk/openai custom providers that -// declare baseURL and an apiKey-compatible setting. The `olpx` id itself is +// declare baseURL and an apiKey-compatible setting. The synced provider id itself is // skipped so sync output is not re-imported. func ImportCustomProviders(raw Raw) []ImportableProvider { out := []ImportableProvider{} provRaw, _ := raw["provider"].(map[string]any) for id, v := range provRaw { - if id == "olpx" { + if id == ProviderKey { continue } m, ok := v.(map[string]any) diff --git a/internal/opencode/opencode_test.go b/internal/opencode/opencode_test.go index a16fe1b..59ffd7f 100644 --- a/internal/opencode/opencode_test.go +++ b/internal/opencode/opencode_test.go @@ -64,35 +64,35 @@ func TestResolveGlobalConfigPathPrecedence(t *testing.T) { } } -func TestValidateOLPXProvider(t *testing.T) { +func TestValidateOcswitchProvider(t *testing.T) { raw := Raw{} aliases := []string{"gpt-5.4", "gpt-5.4-mini"} baseURL := "http://127.0.0.1:9982/v1" - apiKey := "olpx-local" - EnsureOLPXProvider(raw, baseURL, apiKey, aliases) + apiKey := "ocswitch-local" + EnsureOcswitchProvider(raw, baseURL, apiKey, aliases) providerRaw, _ := raw["provider"].(map[string]any) - olpxRaw, _ := providerRaw["olpx"].(map[string]any) - opts, _ := olpxRaw["options"].(map[string]any) + providerEntry, _ := providerRaw[ProviderKey].(map[string]any) + opts, _ := providerEntry["options"].(map[string]any) if got, ok := opts["setCacheKey"].(bool); !ok || !got { - t.Fatalf("provider.olpx.options.setCacheKey = %#v, want true", opts["setCacheKey"]) + t.Fatalf("provider.%s.options.setCacheKey = %#v, want true", ProviderKey, opts["setCacheKey"]) } - if err := ValidateOLPXProvider(raw, baseURL, apiKey, aliases); err != nil { - t.Fatalf("ValidateOLPXProvider() unexpected error: %v", err) + if err := ValidateOcswitchProvider(raw, baseURL, apiKey, aliases); err != nil { + t.Fatalf("ValidateOcswitchProvider() unexpected error: %v", err) } } -func TestEnsureOLPXProviderPreservesExistingModelMetadata(t *testing.T) { +func TestEnsureOcswitchProviderPreservesExistingModelMetadata(t *testing.T) { raw := Raw{ "$schema": "https://opencode.ai/config.json", "provider": map[string]any{ - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, "models": map[string]any{ @@ -116,14 +116,14 @@ func TestEnsureOLPXProviderPreservesExistingModelMetadata(t *testing.T) { }, } - changed := EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}) + changed := EnsureOcswitchProvider(raw, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}) if changed { - t.Fatal("EnsureOLPXProvider() reported change for unchanged same-name alias") + t.Fatal("EnsureOcswitchProvider() reported change for unchanged same-name alias") } providerRaw := raw["provider"].(map[string]any) - olpxRaw := providerRaw["olpx"].(map[string]any) - models := olpxRaw["models"].(map[string]any) + providerEntry := providerRaw[ProviderKey].(map[string]any) + models := providerEntry["models"].(map[string]any) model := models["gpt-5.4"].(map[string]any) if got := model["name"]; got != "custom-display-name" { t.Fatalf("model name = %#v, want custom-display-name preserved", got) @@ -142,16 +142,16 @@ func TestEnsureOLPXProviderPreservesExistingModelMetadata(t *testing.T) { } } -func TestEnsureOLPXProviderDoesNotPanicOnComparableMetadata(t *testing.T) { +func TestEnsureOcswitchProviderDoesNotPanicOnComparableMetadata(t *testing.T) { raw := Raw{ "$schema": "https://opencode.ai/config.json", "provider": map[string]any{ - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, "models": map[string]any{ @@ -169,25 +169,25 @@ func TestEnsureOLPXProviderDoesNotPanicOnComparableMetadata(t *testing.T) { defer func() { if r := recover(); r != nil { - t.Fatalf("EnsureOLPXProvider() panicked with slice metadata: %v", r) + t.Fatalf("EnsureOcswitchProvider() panicked with slice metadata: %v", r) } }() - changed := EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}) + changed := EnsureOcswitchProvider(raw, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}) if changed { - t.Fatal("EnsureOLPXProvider() reported change for unchanged alias metadata with slices") + t.Fatal("EnsureOcswitchProvider() reported change for unchanged alias metadata with slices") } } -func TestValidateOLPXProviderAllowsCustomModelMetadata(t *testing.T) { +func TestValidateOcswitchProviderAllowsCustomModelMetadata(t *testing.T) { raw := Raw{ "provider": map[string]any{ - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, "models": map[string]any{ @@ -200,62 +200,62 @@ func TestValidateOLPXProviderAllowsCustomModelMetadata(t *testing.T) { }, } - if err := ValidateOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}); err != nil { - t.Fatalf("ValidateOLPXProvider() unexpected error for custom metadata: %v", err) + if err := ValidateOcswitchProvider(raw, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}); err != nil { + t.Fatalf("ValidateOcswitchProvider() unexpected error for custom metadata: %v", err) } } -func TestRenderSaveDataReplacesExistingProviderOLPXOnly(t *testing.T) { +func TestRenderSaveDataReplacesExistingProviderOnly(t *testing.T) { raw := Raw{ "$schema": "https://opencode.ai/config.json", - "model": "olpx/gpt-5.4", + "model": "ocswitch/gpt-5.4", "provider": map[string]any{ "anthropic": map[string]any{"npm": "@ai-sdk/anthropic"}, - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, "models": map[string]any{"gpt-5.4": map[string]any{"name": "gpt-5.4"}}, }, "openai": map[string]any{"npm": "@ai-sdk/openai"}, }, - "small_model": "olpx/gpt-5.4-mini", + "small_model": "ocswitch/gpt-5.4-mini", } - original := []byte("{\n \"model\": \"olpx/old\",\n \"provider\": {\n \"anthropic\": {\"npm\": \"@ai-sdk/anthropic\"},\n \"olpx\": {\n \"npm\": \"old\",\n \"options\": {\"baseURL\": \"http://old/v1\"},\n \"models\": {\"old\": {\"name\": \"old\"}}\n },\n \"openai\": {\"npm\": \"@ai-sdk/openai\"}\n },\n \"small_model\": \"olpx/old-mini\"\n}\n") + original := []byte("{\n \"model\": \"ocswitch/old\",\n \"provider\": {\n \"anthropic\": {\"npm\": \"@ai-sdk/anthropic\"},\n \"ocswitch\": {\n \"npm\": \"old\",\n \"options\": {\"baseURL\": \"http://old/v1\"},\n \"models\": {\"old\": {\"name\": \"old\"}}\n },\n \"openai\": {\"npm\": \"@ai-sdk/openai\"}\n },\n \"small_model\": \"ocswitch/old-mini\"\n}\n") - got, err := patchProviderOLPXDocument(original, raw) + got, err := patchProviderDocument(original, raw) if err != nil { - t.Fatalf("patchProviderOLPXDocument() error: %v", err) + t.Fatalf("patchProviderDocument() error: %v", err) } assertValidJSON(t, got) assertStringOrder(t, string(got), []string{`"model"`, `"provider"`, `"small_model"`}) - assertStringOrder(t, string(got), []string{`"anthropic"`, `"olpx"`, `"openai"`}) + assertStringOrder(t, string(got), []string{`"anthropic"`, `"ocswitch"`, `"openai"`}) if strings.Contains(string(got), `"npm": "old"`) { - t.Fatalf("old provider.olpx content still present: %s", string(got)) + t.Fatalf("old provider.ocswitch content still present: %s", string(got)) } var saved Raw if err := json.Unmarshal(got, &saved); err != nil { t.Fatalf("unmarshal patched json: %v", err) } - if err := ValidateOLPXProvider(saved, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}); err != nil { - t.Fatalf("ValidateOLPXProvider(saved) error: %v", err) + if err := ValidateOcswitchProvider(saved, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}); err != nil { + t.Fatalf("ValidateOcswitchProvider(saved) error: %v", err) } } -func TestRenderSaveDataInsertsOLPXWithoutReorderingProviderKeys(t *testing.T) { +func TestRenderSaveDataInsertsOcswitchWithoutReorderingProviderKeys(t *testing.T) { raw := Raw{ "provider": map[string]any{ "anthropic": map[string]any{"npm": "@ai-sdk/anthropic"}, - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, "models": map[string]any{"gpt-5.4": map[string]any{"name": "gpt-5.4"}}, @@ -263,38 +263,38 @@ func TestRenderSaveDataInsertsOLPXWithoutReorderingProviderKeys(t *testing.T) { "openai": map[string]any{"npm": "@ai-sdk/openai"}, }, } - original := []byte("{\n \"provider\": {\n \"anthropic\": {\"npm\": \"@ai-sdk/anthropic\"},\n \"openai\": {\"npm\": \"@ai-sdk/openai\"}\n },\n \"model\": \"olpx/gpt-5.4\"\n}\n") + original := []byte("{\n \"provider\": {\n \"anthropic\": {\"npm\": \"@ai-sdk/anthropic\"},\n \"openai\": {\"npm\": \"@ai-sdk/openai\"}\n },\n \"model\": \"ocswitch/gpt-5.4\"\n}\n") - got, err := patchProviderOLPXDocument(original, raw) + got, err := patchProviderDocument(original, raw) if err != nil { - t.Fatalf("patchProviderOLPXDocument() error: %v", err) + t.Fatalf("patchProviderDocument() error: %v", err) } assertValidJSON(t, got) - assertStringOrder(t, string(got), []string{`"anthropic"`, `"openai"`, `"olpx"`}) + assertStringOrder(t, string(got), []string{`"anthropic"`, `"openai"`, `"ocswitch"`}) } func TestRenderSaveDataInsertsProviderAtTopLevelEnd(t *testing.T) { raw := Raw{ - "model": "olpx/gpt-5.4", + "model": "ocswitch/gpt-5.4", "provider": map[string]any{ - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, "models": map[string]any{"gpt-5.4": map[string]any{"name": "gpt-5.4"}}, }, }, - "small_model": "olpx/gpt-5.4-mini", + "small_model": "ocswitch/gpt-5.4-mini", } - original := []byte("{\n \"model\": \"olpx/gpt-5.4\",\n \"small_model\": \"olpx/gpt-5.4-mini\"\n}\n") + original := []byte("{\n \"model\": \"ocswitch/gpt-5.4\",\n \"small_model\": \"ocswitch/gpt-5.4-mini\"\n}\n") - got, err := patchProviderOLPXDocument(original, raw) + got, err := patchProviderDocument(original, raw) if err != nil { - t.Fatalf("patchProviderOLPXDocument() error: %v", err) + t.Fatalf("patchProviderDocument() error: %v", err) } assertValidJSON(t, got) assertStringOrder(t, string(got), []string{`"model"`, `"small_model"`, `"provider"`}) @@ -303,12 +303,12 @@ func TestRenderSaveDataInsertsProviderAtTopLevelEnd(t *testing.T) { func TestRenderSaveDataAcceptsJSONCAndProducesValidJSON(t *testing.T) { raw := Raw{ "provider": map[string]any{ - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, "models": map[string]any{"gpt-5.4": map[string]any{"name": "gpt-5.4"}}, @@ -317,9 +317,9 @@ func TestRenderSaveDataAcceptsJSONCAndProducesValidJSON(t *testing.T) { } original := []byte("{\n // comment\n \"provider\": {\n \"openai\": {\"npm\": \"@ai-sdk/openai\"},\n },\n}\n") - got, err := patchProviderOLPXDocument(original, raw) + got, err := patchProviderDocument(original, raw) if err != nil { - t.Fatalf("patchProviderOLPXDocument() error: %v", err) + t.Fatalf("patchProviderDocument() error: %v", err) } assertValidJSON(t, got) if bytes.Contains(got, []byte("// comment")) { @@ -330,51 +330,51 @@ func TestRenderSaveDataAcceptsJSONCAndProducesValidJSON(t *testing.T) { func TestRenderSaveDataRejectsInvalidJSONC(t *testing.T) { raw := Raw{ "provider": map[string]any{ - "olpx": map[string]any{ + ProviderKey: map[string]any{ "npm": "@ai-sdk/openai", - "name": "OpenCode LocalProxy CLI", + "name": ProviderName, "options": map[string]any{ "baseURL": "http://127.0.0.1:9982/v1", - "apiKey": "olpx-local", + "apiKey": "ocswitch-local", "setCacheKey": true, }, }, }, } - if _, err := patchProviderOLPXDocument([]byte(`{"provider": {`), raw); err == nil { + if _, err := patchProviderDocument([]byte(`{"provider": {`), raw); err == nil { t.Fatal("expected invalid json/jsonc error") } } func TestRenderSaveDataRejectsNonObjectProvider(t *testing.T) { raw := Raw{} - EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}) + EnsureOcswitchProvider(raw, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}) - if _, err := patchProviderOLPXDocument([]byte(`{"provider":"bad"}`), raw); err == nil { + if _, err := patchProviderDocument([]byte(`{"provider":"bad"}`), raw); err == nil { t.Fatal("expected provider object error") } } func TestRenderSaveDataRejectsNonObjectTopLevel(t *testing.T) { raw := Raw{} - EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}) + EnsureOcswitchProvider(raw, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}) - if _, err := patchProviderOLPXDocument([]byte(`[]`), raw); err == nil { + if _, err := patchProviderDocument([]byte(`[]`), raw); err == nil { t.Fatal("expected top-level object error") } - if _, err := patchProviderOLPXDocument([]byte("{} trailing"), raw); err == nil { + if _, err := patchProviderDocument([]byte(`{} trailing`), raw); err == nil { t.Fatal("expected single top-level object error") } } func TestRenderSaveDataWritesValidJSONToDisk(t *testing.T) { path := filepath.Join(t.TempDir(), "opencode.jsonc") - if err := os.WriteFile(path, []byte("{\n \"model\": \"olpx/gpt-5.4\",\n \"provider\": {\n \"openai\": {\"npm\": \"@ai-sdk/openai\"}\n }\n}\n"), 0o600); err != nil { + if err := os.WriteFile(path, []byte("{\n \"model\": \"ocswitch/gpt-5.4\",\n \"provider\": {\n \"openai\": {\"npm\": \"@ai-sdk/openai\"}\n }\n}\n"), 0o600); err != nil { t.Fatalf("write seed config: %v", err) } raw := Raw{} - EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}) + EnsureOcswitchProvider(raw, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}) if err := Save(path, raw); err != nil { t.Fatalf("Save() error: %v", err) @@ -388,14 +388,14 @@ func TestRenderSaveDataWritesValidJSONToDisk(t *testing.T) { if err != nil { t.Fatalf("Load(saved) error: %v", err) } - if err := ValidateOLPXProvider(loaded, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}); err != nil { - t.Fatalf("ValidateOLPXProvider(loaded) error: %v", err) + if err := ValidateOcswitchProvider(loaded, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}); err != nil { + t.Fatalf("ValidateOcswitchProvider(loaded) error: %v", err) } } func TestSavePreservesExistingModelMetadataForSameAlias(t *testing.T) { path := filepath.Join(t.TempDir(), "opencode.jsonc") - seed := []byte("{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"provider\": {\n \"olpx\": {\n \"npm\": \"@ai-sdk/openai\",\n \"name\": \"OpenCode LocalProxy CLI\",\n \"options\": {\n \"baseURL\": \"http://127.0.0.1:9982/v1\",\n \"apiKey\": \"olpx-local\",\n \"setCacheKey\": true\n },\n \"models\": {\n \"gpt-5.4\": {\n \"name\": \"custom-display-name\",\n \"limit\": {\n \"context\": 272000,\n \"output\": 128000\n },\n \"options\": {\n \"serviceTier\": \"priority\"\n }\n }\n }\n }\n }\n}\n") + seed := []byte("{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"provider\": {\n \"ocswitch\": {\n \"npm\": \"@ai-sdk/openai\",\n \"name\": \"OpenCode Provider Switch CLI\",\n \"options\": {\n \"baseURL\": \"http://127.0.0.1:9982/v1\",\n \"apiKey\": \"ocswitch-local\",\n \"setCacheKey\": true\n },\n \"models\": {\n \"gpt-5.4\": {\n \"name\": \"custom-display-name\",\n \"limit\": {\n \"context\": 272000,\n \"output\": 128000\n },\n \"options\": {\n \"serviceTier\": \"priority\"\n }\n }\n }\n }\n }\n}\n") if err := os.WriteFile(path, seed, 0o600); err != nil { t.Fatalf("write seed config: %v", err) } @@ -404,9 +404,9 @@ func TestSavePreservesExistingModelMetadataForSameAlias(t *testing.T) { if err != nil { t.Fatalf("Load(seed) error: %v", err) } - changed := EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"}) + changed := EnsureOcswitchProvider(raw, "http://127.0.0.1:9982/v1", "ocswitch-local", []string{"gpt-5.4"}) if changed { - t.Fatal("EnsureOLPXProvider() reported change for preserved same-name alias metadata") + t.Fatal("EnsureOcswitchProvider() reported change for preserved same-name alias metadata") } if err := Save(path, raw); err != nil { t.Fatalf("Save() error: %v", err) @@ -417,8 +417,8 @@ func TestSavePreservesExistingModelMetadataForSameAlias(t *testing.T) { t.Fatalf("Load(saved) error: %v", err) } providerRaw := loaded["provider"].(map[string]any) - olpxRaw := providerRaw["olpx"].(map[string]any) - models := olpxRaw["models"].(map[string]any) + providerEntry := providerRaw[ProviderKey].(map[string]any) + models := providerEntry["models"].(map[string]any) model := models["gpt-5.4"].(map[string]any) if got := model["name"]; got != "custom-display-name" { t.Fatalf("saved model name = %#v, want custom-display-name preserved", got) diff --git a/internal/proxy/server.go b/internal/proxy/server.go index 9e30286..f4d192a 100644 --- a/internal/proxy/server.go +++ b/internal/proxy/server.go @@ -1,5 +1,5 @@ // Package proxy implements the local `/v1/responses` HTTP server that resolves -// olpx aliases and forwards requests to upstream providers with deterministic +// ocswitch aliases and forwards requests to upstream providers with deterministic // pre-first-byte failover. package proxy @@ -17,7 +17,7 @@ import ( "sync/atomic" "time" - "github.com/anomalyco/opencode-provider-switch/internal/config" + "github.com/Apale7/opencode-provider-switch/internal/config" ) var firstByteTimeout = 15 * time.Second @@ -32,7 +32,7 @@ type openAIError struct { Code string `json:"code,omitempty"` } -// Server is the local olpx HTTP proxy. +// Server is the local ocswitch HTTP proxy. type Server struct { cfg *config.Config client *http.Client @@ -62,7 +62,7 @@ func New(cfg *config.Config) *Server { Transport: transport, Timeout: 0, // streaming, no overall timeout }, - logger: log.New(log.Writer(), "[olpx] ", log.LstdFlags|log.Lmicroseconds), + logger: log.New(log.Writer(), "[ocswitch] ", log.LstdFlags|log.Lmicroseconds), } } @@ -109,7 +109,7 @@ func (s *Server) handleModels(w http.ResponseWriter, r *http.Request) { data = append(data, map[string]any{ "id": aliasName, "object": "model", - "owned_by": "olpx", + "owned_by": config.AppName, }) } w.Header().Set("Content-Type", "application/json") @@ -371,7 +371,7 @@ func errorTypeForStatus(status int) string { } func normalizeAliasName(model string) string { - const prefix = "olpx/" + prefix := config.AppName + "/" if strings.HasPrefix(model, prefix) { trimmed := strings.TrimPrefix(model, prefix) if trimmed != "" { @@ -381,14 +381,14 @@ func normalizeAliasName(model string) string { return model } -// writeDebugHeaders sets the X-OLPX-* debug headers before WriteHeader. +// writeDebugHeaders sets the X-OCSWITCH-* debug headers before WriteHeader. func (s *Server) writeDebugHeaders(w http.ResponseWriter, alias, provider, remoteModel string, attempt, failoverCount int) { h := w.Header() - h.Set("X-OLPX-Alias", alias) - h.Set("X-OLPX-Provider", provider) - h.Set("X-OLPX-Remote-Model", remoteModel) - h.Set("X-OLPX-Attempt", fmt.Sprintf("%d", attempt)) - h.Set("X-OLPX-Failover-Count", fmt.Sprintf("%d", failoverCount)) + h.Set("X-OCSWITCH-Alias", alias) + h.Set("X-OCSWITCH-Provider", provider) + h.Set("X-OCSWITCH-Remote-Model", remoteModel) + h.Set("X-OCSWITCH-Attempt", fmt.Sprintf("%d", attempt)) + h.Set("X-OCSWITCH-Failover-Count", fmt.Sprintf("%d", failoverCount)) } // hopByHopHeaders lists headers that must not be forwarded per RFC 7230. diff --git a/internal/proxy/server_test.go b/internal/proxy/server_test.go index a9f7cdf..df51c50 100644 --- a/internal/proxy/server_test.go +++ b/internal/proxy/server_test.go @@ -11,17 +11,17 @@ import ( "testing" "time" - "github.com/anomalyco/opencode-provider-switch/internal/config" + "github.com/Apale7/opencode-provider-switch/internal/config" ) func TestHandleResponsesWritesOpenAIErrorForMissingAlias(t *testing.T) { t.Parallel() srv := New(&config.Config{ - Server: config.Server{APIKey: "olpx-local"}, + Server: config.Server{APIKey: config.DefaultLocalAPIKey}, }) req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"missing","stream":true}`)) - req.Header.Set("Authorization", "Bearer olpx-local") + req.Header.Set("Authorization", "Bearer "+config.DefaultLocalAPIKey) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() @@ -64,7 +64,7 @@ func TestHandleResponsesFailsOverOn429(t *testing.T) { defer second.Close() srv := New(&config.Config{ - Server: config.Server{APIKey: "olpx-local"}, + Server: config.Server{APIKey: config.DefaultLocalAPIKey}, Providers: []config.Provider{ {ID: "p1", BaseURL: first.URL + "/v1", APIKey: "sk-1"}, {ID: "p2", BaseURL: second.URL + "/v1", APIKey: "sk-2"}, @@ -76,8 +76,8 @@ func TestHandleResponsesFailsOverOn429(t *testing.T) { }}, }) - req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"olpx/gpt-5.4","stream":true}`)) - req.Header.Set("Authorization", "Bearer olpx-local") + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"ocswitch/gpt-5.4","stream":true}`)) + req.Header.Set("Authorization", "Bearer "+config.DefaultLocalAPIKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "text/event-stream") rr := httptest.NewRecorder() @@ -96,14 +96,14 @@ func TestHandleResponsesFailsOverOn429(t *testing.T) { if secondSeenModel != "up-2" { t.Fatalf("second upstream model = %q, want up-2", secondSeenModel) } - if got := rr.Header().Get("X-OLPX-Attempt"); got != "2" { - t.Fatalf("X-OLPX-Attempt = %q, want 2", got) + if got := rr.Header().Get("X-OCSWITCH-Attempt"); got != "2" { + t.Fatalf("X-OCSWITCH-Attempt = %q, want 2", got) } - if got := rr.Header().Get("X-OLPX-Failover-Count"); got != "1" { - t.Fatalf("X-OLPX-Failover-Count = %q, want 1", got) + if got := rr.Header().Get("X-OCSWITCH-Failover-Count"); got != "1" { + t.Fatalf("X-OCSWITCH-Failover-Count = %q, want 1", got) } - if got := rr.Header().Get("X-OLPX-Provider"); got != "p2" { - t.Fatalf("X-OLPX-Provider = %q, want p2", got) + if got := rr.Header().Get("X-OCSWITCH-Provider"); got != "p2" { + t.Fatalf("X-OCSWITCH-Provider = %q, want p2", got) } } @@ -124,7 +124,7 @@ func TestHandleResponsesDoesNotFailOverOn400(t *testing.T) { defer second.Close() srv := New(&config.Config{ - Server: config.Server{APIKey: "olpx-local"}, + Server: config.Server{APIKey: config.DefaultLocalAPIKey}, Providers: []config.Provider{ {ID: "p1", BaseURL: first.URL + "/v1"}, {ID: "p2", BaseURL: second.URL + "/v1"}, @@ -137,7 +137,7 @@ func TestHandleResponsesDoesNotFailOverOn400(t *testing.T) { }) req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5.4","stream":true}`)) - req.Header.Set("Authorization", "Bearer olpx-local") + req.Header.Set("Authorization", "Bearer "+config.DefaultLocalAPIKey) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() @@ -149,8 +149,8 @@ func TestHandleResponsesDoesNotFailOverOn400(t *testing.T) { if calledSecond { t.Fatal("second upstream should not be called for 400 response") } - if got := rr.Header().Get("X-OLPX-Provider"); got != "p1" { - t.Fatalf("X-OLPX-Provider = %q, want p1", got) + if got := rr.Header().Get("X-OCSWITCH-Provider"); got != "p1" { + t.Fatalf("X-OCSWITCH-Provider = %q, want p1", got) } if body := rr.Body.String(); body != `{"error":{"message":"bad request"}}` { t.Fatalf("body = %q", body) @@ -181,7 +181,7 @@ func TestHandleResponsesSkipsDisabledProviders(t *testing.T) { defer enabled.Close() srv := New(&config.Config{ - Server: config.Server{APIKey: "olpx-local"}, + Server: config.Server{APIKey: config.DefaultLocalAPIKey}, Providers: []config.Provider{ {ID: "p1", BaseURL: disabled.URL + "/v1", Disabled: true}, {ID: "p2", BaseURL: enabled.URL + "/v1"}, @@ -194,7 +194,7 @@ func TestHandleResponsesSkipsDisabledProviders(t *testing.T) { }) req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5.4","stream":true}`)) - req.Header.Set("Authorization", "Bearer olpx-local") + req.Header.Set("Authorization", "Bearer "+config.DefaultLocalAPIKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "text/event-stream") rr := httptest.NewRecorder() @@ -210,14 +210,14 @@ func TestHandleResponsesSkipsDisabledProviders(t *testing.T) { if seenModel != "up-2" { t.Fatalf("enabled upstream model = %q, want up-2", seenModel) } - if got := rr.Header().Get("X-OLPX-Attempt"); got != "1" { - t.Fatalf("X-OLPX-Attempt = %q, want 1", got) + if got := rr.Header().Get("X-OCSWITCH-Attempt"); got != "1" { + t.Fatalf("X-OCSWITCH-Attempt = %q, want 1", got) } - if got := rr.Header().Get("X-OLPX-Failover-Count"); got != "0" { - t.Fatalf("X-OLPX-Failover-Count = %q, want 0", got) + if got := rr.Header().Get("X-OCSWITCH-Failover-Count"); got != "0" { + t.Fatalf("X-OCSWITCH-Failover-Count = %q, want 0", got) } - if got := rr.Header().Get("X-OLPX-Provider"); got != "p2" { - t.Fatalf("X-OLPX-Provider = %q, want p2", got) + if got := rr.Header().Get("X-OCSWITCH-Provider"); got != "p2" { + t.Fatalf("X-OCSWITCH-Provider = %q, want p2", got) } if body := rr.Body.String(); body != "data: ok\n\n" { t.Fatalf("body = %q, want SSE payload", body) @@ -228,7 +228,7 @@ func TestHandleModelsSkipsAliasesWithoutAvailableTargets(t *testing.T) { t.Parallel() srv := New(&config.Config{ - Server: config.Server{APIKey: "olpx-local"}, + Server: config.Server{APIKey: config.DefaultLocalAPIKey}, Providers: []config.Provider{ {ID: "p1", BaseURL: "https://p1.example.com/v1"}, {ID: "p2", BaseURL: "https://p2.example.com/v1", Disabled: true}, @@ -241,7 +241,7 @@ func TestHandleModelsSkipsAliasesWithoutAvailableTargets(t *testing.T) { }) req := httptest.NewRequest(http.MethodGet, "/v1/models", nil) - req.Header.Set("Authorization", "Bearer olpx-local") + req.Header.Set("Authorization", "Bearer "+config.DefaultLocalAPIKey) rr := httptest.NewRecorder() srv.handleModels(rr, req)