# opencode-litellm

Runtime LiteLLM model autodiscovery for OpenCode. The plugin fetches LiteLLM deployment and model-group metadata at startup and adds an in-memory provider named `LiteLLM` by default.

## Install

Install the package by npm name, then add it to OpenCode `plugin` config:

```sh
npm install @staticeng/opencode-litellm
```

```json
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    [
      "@staticeng/opencode-litellm",
      {
        "baseURL": "https://litellm.example.com/v1",
        "apiKeyEnv": "LITELLM_API_KEY"
      }
    ]
  ]
}
```

Set the API key in your environment before starting OpenCode:

```sh
export LITELLM_API_KEY="..."
```

Do not put real API keys in `opencode.json`. If you pass `apiKey` directly, the plugin uses it only in memory and never logs it, but environment variables are recommended.

Shared or synchronized OpenCode configuration should use the published package name as shown above. Do not use a local path or `file://` plugin reference for shared configuration.

## Options

| Option | Default | Purpose |
| --- | --- | --- |
| `baseURL` | `LITELLM_BASE_URL` | LiteLLM proxy base URL. `/v1` URLs are supported. |
| `apiKey` | env value | Bearer token for LiteLLM. |
| `apiKeyEnv` | `LITELLM_API_KEY` | Environment variable used when `apiKey` is not set. |
| `providerKey` | `LiteLLM` | Key written to legacy `cfg.provider` and V2 `cfg.providers`. |
| `providerName` | `LiteLLM` | Human provider name. |
| `overrides` | `{}` | Per-model deep-merge overrides. |
| `providerOverrides` | `{}` | Provider-level deep-merge overrides. |
| `includeModes` | `chat`, `responses` | LiteLLM modes to include. |
| `includeModels` | all included modes | Optional allowlist by model name. |
| `includeBackends` | unset | Optional exact allowlist by `model_info.backend`, unioned with `includeModels` pins. |
| `excludeModels` | none | Optional denylist by model name. |
| `timeoutMs` | `30000` | Per-request discovery timeout. |
| `strict` | `false` | Throw on fetch/mapping failures instead of warning. |

The LiteLLM key must have permission to call `GET /model/info` and root `GET /model_group/info`.

When `includeBackends` is present, models are candidates when `model_info.backend` exactly and case-sensitively matches an entry or their exact model name is pinned by `includeModels`. Backend values are not trimmed, normalized, or partially matched; missing and non-string values do not match. An empty `includeBackends` matches no backends, so without model pins it includes no models. `excludeModels` always wins over backend matches and pins, and candidates must still pass `includeModes` (or the default `chat` and `responses` modes). When `includeBackends` is omitted, `includeModels` retains its historical behavior: a non-empty array is a model-name allowlist, while an omitted or empty array imposes no model-name allowlist.

## Overrides

LiteLLM metadata does not expose every OpenCode model field. The plugin includes exact built-in contracts for the currently approved GPT-5.4, GPT-5.5, GPT-5.6, DeepSeek V4 Flash, and Qwen3.8 aliases. Use `overrides` to customize those defaults or to add metadata for unknown models.

When LiteLLM publishes `model_info.supported_reasoning_efforts`, the plugin maps that list to OpenCode variants automatically. Known contracts contribute their approved defaults; discovered extra efforts remain available unless that exact contract explicitly closes discovery. Currently only `qwen3.8-flash-next` closes discovery so unverified efforts cannot become active. No substring, suffix, provider namespace, or base-model inference is used.

```json
{
  "plugin": [
    [
      "@staticeng/opencode-litellm",
      {
        "baseURL": "https://litellm.example.com/v1",
        "apiKeyEnv": "LITELLM_API_KEY",
        "overrides": {
          "chatgpt/gpt-5.5": {
            "limit": { "context": 262144, "output": 128000 },
            "variants": {
              "high": { "reasoningEffort": "high" }
            },
            "interleaved": { "field": "reasoning_details" }
          }
        },
        "providerOverrides": {
          "options": {
            "timeout": 600000
          }
        }
      }
    ]
  ]
}
```

Overrides win over built-in contracts and generated values through a recursive deep merge. Model overrides are applied first and provider overrides last. Arrays and scalar values are replaced rather than concatenated.

### GPT Transport

GPT models use `@ai-sdk/openai` and the Responses API through the configured
LiteLLM base URL (for example, `/v1/responses`). Other models retain
`@ai-sdk/openai-compatible` and Chat Completions. No OpenAI OAuth endpoint or
direct upstream URL is introduced.

Selection matches a final `gpt-` plus digit segment (case-insensitive), including
namespaced aliases, in the routable model name, `base_model`, or
`litellm_params.model`. Opaque deployment IDs do not override a named alias.
The routable ID is preserved. O-series and non-GPT models are not migrated merely
because discovery reports `mode: responses`. Explicit model `provider.npm`
overrides still win; V2 uses the corresponding `api.package`.

Legacy reasoning options remain `reasoningEffort` (OpenCode places them under
`providerOptions.openai` for this SDK). V2 GPT request defaults and variants use
Responses wire bodies: `reasoning: { effort }`, not `reasoning_effort`.
Use an OpenCode release with AI SDK 5 or newer, where the OpenAI SDK's
`languageModel()` defaults to Responses. Tests pin the SDK versions used by
OpenCode at qualification time; these are development dependencies only.

After upgrading, pin the exact plugin version and restart only when active
sessions can safely stop. Existing processes retain their loaded plugin; do not
clear global caches to upgrade one plugin.

The built-in mode matrix is:

| Contract | Modes in order | Default |
| --- | --- | --- |
| GPT-5.4 / GPT-5.4 Mini | `none`, `low`, `medium`, `high`, `xhigh` | `none` |
| GPT-5.5 | `none`, `low`, `medium`, `high`, `xhigh` | `medium` |
| GPT-5.6 Luna / Sol / Terra | `none`, `low`, `medium`, `high`, `xhigh`, `max` | `medium` |
| DeepSeek V4 Flash | `off`, `low`, `high`, `max` | `max` |
| Qwen3.8 27B refusal dial | `off`, `low`, `medium`, `xhigh` | `xhigh` |
| Qwen3.8 Flash Next | `off`, `low`, `medium`, `xhigh` | `xhigh` |

DeepSeek `off` sends canonical `none`. Qwen `off` sends only `chat_template_kwargs.enable_thinking=false`, with no reasoning effort. `ultra` is not generated by a built-in contract. Flash Next activates exactly `off`, `low`, `medium`, and `xhigh`: discovered extras such as `ultra` are suppressed, V2 omits `minimal`, `high`, and `max`, and the legacy OpenCode surface disables unsupported generic choices. For requests without an explicit named variant, the plugin supplies the row's contract default through model options; an explicit variant is merged afterward and wins. Qwen's explicit `off` cleanup removes the model's default effort before serialization. The `variantDefault` compatibility marker is UI-only and is stripped before requests for exact built-in aliases.

The exact dynamically discovered alias `qwen3.8-flash-next` also receives verified static metadata that overrides incomplete or conflicting LiteLLM group metadata: text and image input, text output, attachment, reasoning, tool calling, and a 262144-token context. Its output limit remains unknown and is represented by the package's `0` sentinel; pricing and other modalities are not supplied. The contract is still discovery-gated and never creates the alias when LiteLLM omits it.

Normal GPT-5.3 Codex and GPT-5.3 Codex Spark aliases are retired and filtered even if stale discovery metadata still reports them. Unlisted aliases such as `defend/gpt-5.5` do not inherit a contract.

## Runtime Behavior

- Fetches `${baseURL}/model/info`, then root `/model_group/info`, with separate authenticated and timed requests.
- Preserves `/v1` for model-info discovery but resolves model-group discovery from the LiteLLM origin.
- Adds or replaces both legacy `cfg.provider[providerKey]` and V2 `cfg.providers[providerKey]` during the config hook.
- Keeps namespaced deployment IDs and augments them with exact bare model-group IDs; exact collisions are enriched deterministically.
- Skips malformed group rows without a nonblank `model_group` and exposes reasoning capability on legacy and V2 model metadata.
- Applies exact built-in model contracts as defaults, then applies explicit model and provider overrides last.
- Filters retired normal and Spark GPT-5.3 Codex aliases without redirecting them.
- Warns safely and keeps deployment models when group discovery fails in non-strict mode; strict mode mutates neither config surface on failure.
- Does not write `opencode.json`, cache model metadata, or persist generated provider data.
- Emits the required `limit.output` as `0` when LiteLLM reports a context/input limit but no output
  limit. The zero sentinel retains known metadata without inventing a positive output allowance.
- Includes `chat` and `responses` modes by default; non-chat modes such as `embedding` and `rerank` are excluded unless explicitly included.
- No HuggingFace fallback is included in v1.

## Publish Notes

The package is ESM-only and publishes the compiled `dist/` directory.

```sh
npm install
npm run build
npm test
npm pack --dry-run
```

Confirm package contents before publishing and keep secrets out of fixtures, docs, logs, and config examples.

Publishing uses npm trusted publishing from the `npm-production` GitHub environment. Provenance
requires this GitHub repository to remain public. Before a release, configure npm's trusted
publisher for package `@staticeng/opencode-litellm` with this repository, workflow
`publish.yml`, and environment `npm-production`; no npm token is used by the workflow.
