Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement β†’
Sign In

opencode-plugin-litellm

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

opencode-plugin-litellm - npm Package Compare versions

Comparing version
0.4.2
to
0.5.0
+1
-1
package.json
{
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode-plugin-litellm",
"version": "0.4.2",
"version": "0.5.0",
"description": "OpenCode plugin for LiteLLM proxy support with auto-detection and dynamic model discovery",

@@ -6,0 +6,0 @@ "type": "module",

@@ -55,5 +55,2 @@ <div align="center">

"baseURL": "http://localhost:4000/v1"
},
"models": {
"_": { "name": "seed" }
}

@@ -65,7 +62,2 @@ }

> **Why the `"_"` seed model?** OpenCode only activates the plugin's
> model-discovery hook when the provider already has at least one model
> entry. The seed is replaced at startup by the full list from your
> LiteLLM proxy.
```bash

@@ -100,3 +92,3 @@ # 3. Start LiteLLM (if it isn't already)

Point at your LiteLLM proxy and include a seed model so OpenCode registers the provider:
Point at your LiteLLM proxy β€” the plugin discovers all models automatically:

@@ -112,5 +104,2 @@ ```jsonc

"baseURL": "http://localhost:4000/v1"
},
"models": {
"_": { "name": "seed" }
}

@@ -117,0 +106,0 @@ }

import type { Plugin, PluginInput } from '@opencode-ai/plugin'
import { discoverBucket } from './discover'
import {
autoDetectLiteLLM,
checkLiteLLMHealth,
discoverLiteLLMModels,
normalizeBaseURL,
} from '../utils/litellm-api'
import {
formatModelName,
extractModelOwner,
categorizeModel,
} from '../utils/format-model-name'
import type { LiteLLMModel } from '../types'
const CHAT_PROVIDER_ID = 'litellm'
const RESPONSES_PROVIDER_ID = 'litellm-responses'
const DISCOVERY_TIMEOUT_MS = 5000
/**
* Read `customHeaders` from a provider options block.
*/
function readCustomHeaders(
options: Record<string, unknown>,
): Record<string, string> | undefined {
const raw = options.customHeaders
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
const out: Record<string, string> = {}
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v
}
return Object.keys(out).length > 0 ? out : undefined
}
return undefined
}
/**
* Convert a discovered LiteLLM model into an OpenCode config-level
* model entry (the shape used in `provider.*.models` inside
* `opencode.json`).
*/
function toConfigModel(model: LiteLLMModel): Record<string, unknown> {
const type = categorizeModel(model)
const entry: Record<string, unknown> = {
name: formatModelName(model),
}
if (model.max_input_tokens || model.max_output_tokens) {
entry.limit = {
context: model.max_input_tokens ?? 0,
output: model.max_output_tokens ?? 0,
}
}
if (model.supports_function_calling) {
entry.tool_call = true
}
if (model.supports_vision) {
entry.attachment = true
}
if (type === 'embedding' || type === 'image' || type === 'audio') {
// skip non-chat models from the config β€” they can't be used as
// primary chat models and would clutter the picker.
return entry
}
return entry
}
/**
* LiteLLM Plugin for OpenCode.
*
* Implements the `provider.models` hook so OpenCode populates the
* `litellm` provider's model list from a live LiteLLM proxy at
* startup.
* Uses the `config` hook to discover models from a LiteLLM proxy and
* inject them into the provider's `models` map at startup. This is the
* only reliable way to dynamically populate a provider β€” the
* `provider.models` hook is not called by OpenCode for custom providers.
*
* **Important:** OpenCode only calls `provider.models` when the
* provider has at least one model defined in `opencode.json`. You
* must include a seed model entry β€” it can be any model id your
* proxy exposes. The plugin will discover and add all remaining
* models automatically.
*
* By default, every discovered model is registered under the
* `litellm` provider β€” including OpenAI reasoning-tier models like
* `gpt-5*`. Most of those models work fine through
* `/v1/chat/completions` for normal use. If you actually hit the
* "Function tools with reasoning_effort are not supported" error
* from OpenAI, declare a sibling `litellm-responses` provider in
* `opencode.json` (see {@link LiteLLMResponsesPlugin}) and the
* plugin will route those models through `/v1/responses` instead.
*
* Configure the provider in your `opencode.json`:

@@ -40,5 +84,2 @@ *

* "apiKey": "{env:LITELLM_API_KEY}"
* },
* "models": {
* "_": { "name": "seed" }
* }

@@ -51,15 +92,123 @@ * }

return {
provider: {
id: CHAT_PROVIDER_ID,
models: async (provider) => {
// 'all' (not 'chat') by default so reasoning-tier models like
// gpt-5* don't silently disappear when the user hasn't opted
// into the responses-API split. The split only kicks in when
// the user explicitly declares a `litellm-responses` provider.
return discoverBucket('all', provider, {
id: CHAT_PROVIDER_ID,
url: '',
config: async (config: any) => {
// Ensure the provider entry exists
if (!config.provider) config.provider = {}
const existing = config.provider[CHAT_PROVIDER_ID] as
| Record<string, unknown>
| undefined
const options = (existing?.options ?? {}) as Record<string, unknown>
const configuredBase =
typeof options.baseURL === 'string' ? options.baseURL : undefined
const configuredKey =
typeof options.apiKey === 'string' && options.apiKey
? options.apiKey
: undefined
const envKey =
process.env.LITELLM_API_KEY ?? process.env.LITELLM_MASTER_KEY
const apiKey = configuredKey ?? envKey
const customHeaders = readCustomHeaders(options)
// Resolve base URL
let baseURL: string | null = null
if (configuredBase) {
baseURL = normalizeBaseURL(configuredBase)
} else {
baseURL = await autoDetectLiteLLM(apiKey, customHeaders)
}
if (!baseURL) {
console.warn(
'[opencode-litellm] No LiteLLM proxy found. Configure provider.litellm.options.baseURL or start LiteLLM on port 4000/8000/8080.',
)
return
}
// Create provider entry if it doesn't exist
if (!existing) {
config.provider[CHAT_PROVIDER_ID] = {
npm: '@ai-sdk/openai-compatible',
})
},
name: 'LiteLLM (proxy)',
options: {
baseURL: `${baseURL}/v1`,
},
models: {},
}
}
const provider = config.provider[CHAT_PROVIDER_ID] as Record<
string,
unknown
>
// Ensure npm is set
if (!provider.npm) {
provider.npm = '@ai-sdk/openai-compatible'
}
// Ensure options.baseURL is set
if (!provider.options) {
provider.options = { baseURL: `${baseURL}/v1` }
}
// Ensure models map exists
if (!provider.models) {
provider.models = {}
}
const models = provider.models as Record<string, unknown>
// Discover models with timeout
const work = async () => {
if (!(await checkLiteLLMHealth(baseURL!, apiKey, customHeaders))) {
console.warn(
`[opencode-litellm] LiteLLM appears offline or unauthorized at ${baseURL}`,
)
return
}
let discovered: LiteLLMModel[]
try {
discovered = await discoverLiteLLMModels(
baseURL!,
apiKey,
customHeaders,
)
} catch (error) {
console.warn(
'[opencode-litellm] Model discovery failed:',
error instanceof Error ? error.message : String(error),
)
return
}
if (discovered.length === 0) {
console.warn(
'[opencode-litellm] LiteLLM responded but exposed zero models.',
)
return
}
for (const model of discovered) {
// Don't overwrite user-curated entries
if (models[model.id]) continue
models[model.id] = toConfigModel(model)
}
// Remove the seed placeholder if real models were discovered
if (models['_'] && Object.keys(models).length > 1) {
delete models['_']
}
console.log(
`[opencode-litellm] Discovered ${discovered.length} models from ${baseURL}`,
)
}
await Promise.race([
work(),
new Promise<void>((resolve) =>
setTimeout(resolve, DISCOVERY_TIMEOUT_MS),
),
])
},

@@ -69,46 +218,6 @@ }

/**
* Optional sibling plugin that registers the `litellm-responses`
* provider for OpenAI reasoning-tier models.
*
* OpenAI's reasoning-tier models (`gpt-5*`, `o1`/`o3`/`o4*`) reject
* requests that combine `reasoning_effort` with function tools when
* sent to `/v1/chat/completions`. Routing those models through
* `/v1/responses` (the OpenAI Responses API) avoids the rejection.
*
* This plugin is only active when the `litellm-responses` provider
* is declared in `opencode.json`. When it is, the chat-only `litellm`
* provider continues to register every discovered model β€” set
* `chatApiModels` / `responsesApiModels` on either provider's options
* if you want to control which side a particular model lives on.
*
* Example `opencode.json` snippet:
*
* {
* "provider": {
* "litellm-responses": {
* "npm": "@ai-sdk/openai",
* "name": "LiteLLM (responses)",
* "options": {
* "baseURL": "http://localhost:4000/v1",
* "apiKey": "{env:LITELLM_API_KEY}",
* "compatibility": "strict"
* }
* }
* }
* }
*/
// Re-export the responses plugin for backwards compat, but it's now a no-op.
// The config hook approach handles all models in a single provider.
export const LiteLLMResponsesPlugin: Plugin = async (_input: PluginInput) => {
return {
provider: {
id: RESPONSES_PROVIDER_ID,
models: async (provider) => {
return discoverBucket('responses', provider, {
id: RESPONSES_PROVIDER_ID,
url: '',
npm: '@ai-sdk/openai',
})
},
},
}
return {}
}