Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@

### Bug Fixes

* **v2:** read the connect dialog's Base URL and Provider ID from the credential's `metadata`, where OpenCode 2.1+ stores them
* **v1:** after a login rebuilds the plugin (as `/connect` and the Desktop app do), read the new credential instead of a cached `auth.json`, and stop treating values the plugin wrote into a provider's options on an earlier run as configuration, since OpenCode 1 keeps its global config for the whole process
* prefer a credential saved in the box (the active connection / `/connect` entry) over `LITELLM_API_KEY` / `LITELLM_MASTER_KEY` on both the V1 server path and the OpenCode 2 plugin host; the model cache is now isolated per credential
* **v2:** register Chat Completions models whose deployment accepts Anthropic `cache_control` with `compatibility.cacheFormat: "anthropic"` so prompt caching works for Claude behind LiteLLM
* **v2:** spell reasoning-effort variants as `thinking` + `effort` for providers on an Anthropic runtime package, where `reasoningEffort` is ignored; models without adaptive thinking get none there
Expand All @@ -99,6 +101,7 @@

### Features

* sign in with LiteLLM SSO (the CLI-auth PKCE contract, CLI SSO, or a token pasted from the LiteLLM UI) or directly with an OpenID Connect identity provider, from the connect dialog of OpenCode 1 and 2, their Desktop apps, and `opencode auth login`; the token is refreshed and sent with every request, and the model cache of a sign-in survives its token rotating
* register a LiteLLM integration with OpenCode 2's connect-provider dialog so gateways can be connected entirely from the web UI (API key plus an optional base URL/provider id form), with the stored key used for discovery and injected by OpenCode at request time
* **v2:** add the `modelCompatibility` option to set or remove the inferred `cacheFormat` per model
* route models LiteLLM reports with `mode: "responses"` through the OpenAI Responses API (per-model runtime package on OpenCode 2; a `<provider>-responses` sibling on OpenCode 1)
Expand Down
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ also shows how to configure a provider directly in `opencode.json`.
| 🧩 **Reasoning-effort variants** | When LiteLLM reports per-model effort support (`supports_low_reasoning_effort`, …), the plugin surfaces each level as a picker variant automatically. |
| 🧭 **Responses-API routing** | Models LiteLLM reports with `mode: "responses"` are invoked through `/v1/responses` instead of `/v1/chat/completions`, with no extra configuration. OpenCode 2 overrides the runtime package per model; OpenCode 1 gets a `<provider>-responses` sibling. |
| πŸ” **Auth-aware** | Honours `providers.litellm.settings.apiKey`, OpenCode `/connect` credentials, the box auth store, and `LITELLM_API_KEY` / `LITELLM_MASTER_KEY` on both the V1 server path and the OpenCode 2 plugin host; on OpenCode 2 a **LiteLLM** entry in the connect-provider dialog (key plus optional base URL) connects a gateway entirely from the web UI. A saved credential beats the environment key. |
| πŸͺͺ **Single sign-on** | Signs in through LiteLLM SSO (CLI-auth PKCE, CLI SSO, or a token pasted from the LiteLLM UI) or directly with an OpenID Connect identity provider, from the connect dialog of OpenCode 1 and 2, their Desktop apps, or `opencode auth login`. The token is refreshed and sent with every request. |
| 🌐 **Gateway-friendly** | Supports provider `headers` (and the legacy `customHeaders` option) for proxies behind Cloudflare Access or other API gateways. |
| 🧩 **Splittable catalog** | `includeModels` / `excludeModels` (glob patterns) let one LiteLLM proxy be divided into several OpenCode providers β€” e.g. by naming prefix β€” without hand-maintaining a model list. |
| 🎚️ **Capability overrides** | `modelCapabilities` forces or retracts per-model capability flags (`supports_vision`, `supports_function_calling`, …) when `/v1/model/info` is unavailable or disagrees with your deployment. |
Expand Down Expand Up @@ -327,6 +328,7 @@ If your LiteLLM proxy requires a master key, expose it via any of these approach
| Method | Example |
|---|---|
| Web UI / `/connect` | Run `/connect` (or use the Desktop providers view), pick **LiteLLM**, paste the key, and optionally fill in the base URL and provider id |
| Single sign-on | `/connect` (or the Desktop providers view) β†’ **LiteLLM** β†’ **LiteLLM SSO** or **Identity provider (OIDC)**; see [Single sign-on](#single-sign-on) |
| Env var (base URL) | `LITELLM_BASE_URL=https://litellm.internal.example.com/v1` |
| Env var | `export LITELLM_API_KEY=sk-...` |
| Env var (alias) | `export LITELLM_MASTER_KEY=sk-...` |
Expand All @@ -344,7 +346,36 @@ With no configuration at all, `/connect` β†’ **LiteLLM** is enough:

Precedence when several sources are configured: explicit provider/plugin settings always win. For the key, a credential saved in the box (the active connection or the `/connect` entry) beats the environment (`LITELLM_API_KEY`/`LITELLM_MASTER_KEY`), which is only a fallback; for the base URL, the dialog answer beats `LITELLM_BASE_URL` and local port auto-detection. A provider defined in `opencode.json` (without its own key) picks the saved credential up on the next start, while providers created from the dialog update immediately β€” reconnecting re-points them in place, including a changed base URL or provider id.

The discovered model list is cached per credential: a short hash of the effective key (never the raw key) is part of every cache entry's key, so when the selected credential changes the next discovery starts fresh instead of serving another credential's model list. A `credential.switched` event also recomputes the effective key and reloads the provider registry when it changed β€” including the first credential saved after setup.
#### Single sign-on

The **LiteLLM** entry in `/connect` also offers two browser sign-ins. The sign-in returns to a temporary `http://127.0.0.1` callback, so the browser must run on the same machine as the OpenCode server (OpenCode Desktop's own server qualifies; a Desktop connected to a remote server does not), and the proxy needs an `https` base URL (plain `http` only on a loopback host). On OpenCode 2, OpenCode stores the session as a credential of the integration, refreshes it, and injects it into requests, as it does with a connected key; OpenCode 1 differences are [below](#opencode-1).

**LiteLLM SSO** signs in through the proxy. Leave **Base URL** empty to use the configured provider's URL, the current sign-in's proxy, or `LITELLM_BASE_URL`. A URL that differs from the configured one is refused, because explicit configuration wins in discovery and the token would go to a proxy that did not issue it. The plugin uses the newest flow the proxy offers:

1. **CLI-auth contract** (`/.well-known/litellm-cli-auth`): authorization code + PKCE (`S256`) with a dynamically registered client. Advertised endpoints must stay on the proxy's origin, and these requests do not follow redirects. Access and refresh tokens rotate automatically; a temporary token-endpoint failure (network, 429, 5xx) keeps the current token until it actually expires.
2. **CLI SSO** (`/sso/cli/start`, when the discovery path serves no contract the plugin speaks: a 404, an access gateway's 401 or 403, a page from a catch-all route, or another contract version): confirm the code OpenCode shows in the browser. When your account belongs to several teams, LiteLLM asks for one; the sign-in then fails with the list of teams, so connect again with the team's ID or alias as **Team**. The token lasts as long as the proxy reports (24 hours on proxies that report nothing), then you connect again.
3. **UI token** (when `/sso/cli/start` answers with another error, such as 404 or 405 on older proxies, or 401 or 403 from an access gateway): sign in on the page OpenCode opens and paste the token from the LiteLLM UI as the code. The plugin exchanges it for a virtual key through `/key/generate`, and uses the token itself when the proxy cannot mint one: 403 (no key-generation rights), 404, or 500 (no database). Any other answer, such as 401 for an expired token, fails the sign-in.

A rate limit (429) or server error (5xx) at discovery or `/sso/cli/start` fails the sign-in instead of falling back to an older flow, so trying again can still reach the newer one.

**Identity provider (OIDC)** signs in with your identity provider directly, for proxies running LiteLLM JWT auth (`enable_jwt_auth`) that trust its issuer and accept the client ID as an audience. Enter the `https` issuer URL, a public client ID (no secret) registered for authorization code + PKCE with the redirect `http://127.0.0.1:<port>/callback`, the scopes (default `openid`; some identity providers only issue a refresh token with `offline_access`), and, for identity providers that only accept exact redirect URIs, the ports to try. The `id_token` is the bearer token: the plugin checks its issuer, audience, authorized party, subject, expiry, and nonce, and the proxy verifies its signature. Signing in sends nothing to the proxy, and identity provider requests never carry custom headers.

A sign-in's token only goes to the proxy it is for. Without a configured base URL, the plugin uses the proxy the sign-in came from; a base URL configured for another proxy turns the sign-in off, with a warning in the log. A sign-in that names no proxy, such as an OIDC sign-in with an empty **Base URL**, uses the configured base URL or `LITELLM_BASE_URL`; the plugin does not probe local ports for it, because whatever answered there would get the token. On OpenCode 2 this covers the plugin's own requests, but OpenCode itself pairs the provider whose id is `litellm` with the **LiteLLM** connection and sends that connection's token with the provider's requests. After pointing `litellm` at another proxy, connect again to that proxy, or disconnect.

Disconnecting removes the local credential only; it does not revoke the session on the proxy or the identity provider.

If a sign-in fails before the browser opens (no proxy URL, a plain `http` URL, an unreachable proxy or identity provider, an invalid OIDC field), OpenCode shows only a generic error, such as "Unknown error" in the Desktop app: OpenCode drops the message of an error a plugin raises at that step. The reason is in OpenCode's log. Errors after the browser step, such as the team list, are shown.

##### OpenCode 1

OpenCode 1 lists the sign-ins under the `litellm` provider, in `/connect`, its Desktop app's providers view, and `opencode auth login`. The answers that can be left out sit behind a choice instead of an empty field, since the Desktop app cannot submit one: the proxy (**Configured or previous proxy** or **Another URL…**), the team, the scopes, and the redirect ports. Without a configured or previous proxy, pick **Another URL…**.

- **First sign-in:** run `opencode auth login -p litellm`. OpenCode 1 lists a provider in `/connect` and the Desktop app only once it has models, and the proxy hides them until you sign in. After that, reconnect from either; the model list stays cached for the sign-in, so LiteLLM keeps appearing even after the session ends.
- The session lives in OpenCode 1's `auth.json` like any login (the sign-in's details in `accountId`, the only field OpenCode 1 keeps). The plugin refreshes it shortly before it expires, sends the current token with every request (including the `-responses` sibling), and saves rotated tokens back; when another OpenCode process sharing the file refreshed first, it uses that one.
- A configured `apiKey` still wins over a sign-in, and a sign-in wins over `LITELLM_API_KEY` / `LITELLM_MASTER_KEY`, even once it expired: requests then ask you to connect again.
- Only the default `litellm` provider id can sign in: OpenCode 1 gives a plugin one auth entry. OpenCode 2 imports OpenCode 1 logins under a method this plugin does not claim, so connect again after upgrading.

The discovered model list is cached per credential: a short hash of the effective key (never the raw key) is part of every cache entry's key, so when the selected credential changes the next discovery starts fresh instead of serving another credential's model list. A sign-in is keyed by its credential id instead, because its access token rotates on every refresh. A `credential.switched` event also recomputes the effective key and reloads the provider registry when it changed β€” including the first credential saved after setup.

### Slow proxies (`LITELLM_REQUEST_TIMEOUT_MS`)

Expand Down
Loading
Loading