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.3.1
to
0.4.0
+1
-1
package.json
{
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode-plugin-litellm",
"version": "0.3.1",
"version": "0.4.0",
"description": "OpenCode plugin for LiteLLM proxy support with auto-detection and dynamic model discovery",

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

@@ -74,2 +74,3 @@ <div align="center">

| 🔐 **Auth-aware** | Honours `LITELLM_API_KEY` / `LITELLM_MASTER_KEY` env vars or `provider.litellm.options.apiKey`. |
| 🌐 **Gateway-friendly** | Supports `customHeaders` for proxies behind Cloudflare Access or other API gateways requiring extra HTTP headers. |
| ⏱️ **Non-blocking startup** | Discovery is capped at **5 s** — a slow or offline proxy never delays OpenCode boot. |

@@ -200,2 +201,25 @@ | 🤝 **Non-destructive merge** | Only adds models you don't already have configured. Hand-curated entries are preserved verbatim. |

### Custom headers (Cloudflare Access, API gateways)
If your LiteLLM proxy is behind Cloudflare Access or another gateway that requires extra HTTP headers, use the `customHeaders` option:
```jsonc
{
"provider": {
"litellm": {
"options": {
"baseURL": "https://litellm.internal.example.com/v1",
"apiKey": "{env:LITELLM_API_KEY}",
"customHeaders": {
"CF-Access-Client-Id": "{env:CF_ACCESS_CLIENT_ID}",
"CF-Access-Client-Secret": "{env:CF_ACCESS_CLIENT_SECRET}"
}
}
}
}
}
```
These headers are included in every request the plugin makes during model discovery (health check and `/v1/models`). To obtain a Cloudflare Access Service Token, follow the [Cloudflare docs](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/).
## 🔧 How it works

@@ -291,2 +315,26 @@

<details>
<summary><b>My LiteLLM proxy is behind Cloudflare Access — how do I authenticate?</b></summary>
Cloudflare Access intercepts requests before they reach LiteLLM, so a plain `Authorization: Bearer` header isn't enough. Create a [Cloudflare Access Service Token](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/) and pass the credentials via `customHeaders`:
```jsonc
{
"provider": {
"litellm": {
"options": {
"baseURL": "https://litellm.your-company.com/v1",
"customHeaders": {
"CF-Access-Client-Id": "{env:CF_ACCESS_CLIENT_ID}",
"CF-Access-Client-Secret": "{env:CF_ACCESS_CLIENT_SECRET}"
}
}
}
}
}
```
The `customHeaders` map works for any gateway that requires extra HTTP headers — not just Cloudflare.
</details>
<details>
<summary><b>I get <code>Function tools with reasoning_effort are not supported … in /v1/chat/completions</code> — what do I do?</b></summary>

@@ -293,0 +341,0 @@

@@ -43,5 +43,24 @@ import type { Model as ModelV2, Provider as ProviderV2 } from '@opencode-ai/sdk/v2'

*/
/**
* Extract the `customHeaders` map from the provider options block.
* Returns `undefined` when no custom headers are configured.
*/
function readCustomHeaders(
provider: ProviderV2 | undefined,
): Record<string, string> | undefined {
const options = (provider?.options ?? {}) as Record<string, unknown>
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
}
async function resolveEndpoint(
provider: ProviderV2 | undefined,
): Promise<{ baseURL: string; apiKey?: string } | null> {
): Promise<{ baseURL: string; apiKey?: string; customHeaders?: Record<string, string> } | null> {
const options = (provider?.options ?? {}) as Record<string, unknown>

@@ -51,10 +70,11 @@ const configuredBase = typeof options.baseURL === 'string' ? options.baseURL : undefined

const envKey = process.env.LITELLM_API_KEY ?? process.env.LITELLM_MASTER_KEY
const customHeaders = readCustomHeaders(provider)
if (configuredBase) {
return { baseURL: normalizeBaseURL(configuredBase), apiKey: configuredKey ?? envKey }
return { baseURL: normalizeBaseURL(configuredBase), apiKey: configuredKey ?? envKey, customHeaders }
}
const detected = await autoDetectLiteLLM(configuredKey ?? envKey)
const detected = await autoDetectLiteLLM(configuredKey ?? envKey, customHeaders)
if (!detected) return null
return { baseURL: normalizeBaseURL(detected), apiKey: configuredKey ?? envKey }
return { baseURL: normalizeBaseURL(detected), apiKey: configuredKey ?? envKey, customHeaders }
}

@@ -117,4 +137,4 @@

const { baseURL, apiKey } = endpoint
if (!(await checkLiteLLMHealth(baseURL, apiKey))) {
const { baseURL, apiKey, customHeaders } = endpoint
if (!(await checkLiteLLMHealth(baseURL, apiKey, customHeaders))) {
console.warn(`[opencode-litellm] LiteLLM appears offline or unauthorized at ${baseURL}`)

@@ -126,3 +146,3 @@ return

try {
models = await discoverLiteLLMModels(baseURL, apiKey)
models = await discoverLiteLLMModels(baseURL, apiKey, customHeaders)
} catch (error) {

@@ -129,0 +149,0 @@ console.warn(

@@ -1,2 +0,2 @@

import type { Plugin, PluginInput } from '@opencode-ai/plugin'
import type { Plugin, PluginInput, Config } from '@opencode-ai/plugin'
import { discoverBucket } from './discover'

@@ -8,2 +8,19 @@

/**
* Ensure a provider entry has a `models` map so OpenCode registers it.
*
* OpenCode skips providers that have no models defined in the config,
* which means the `provider.models` hook would never be called. By
* seeding an empty `models` map we guarantee the provider is created
* and the hook has a chance to populate it with discovered models.
*/
function ensureProviderHasModels(config: Config, providerID: string): void {
if (!config.provider) return
const entry = config.provider[providerID]
if (!entry) return
if (!entry.models) {
entry.models = {}
}
}
/**
* LiteLLM Plugin for OpenCode.

@@ -42,2 +59,5 @@ *

return {
config: async (config: Config) => {
ensureProviderHasModels(config, CHAT_PROVIDER_ID)
},
provider: {

@@ -93,2 +113,5 @@ id: CHAT_PROVIDER_ID,

return {
config: async (config: Config) => {
ensureProviderHasModels(config, RESPONSES_PROVIDER_ID)
},
provider: {

@@ -95,0 +118,0 @@ id: RESPONSES_PROVIDER_ID,

@@ -81,2 +81,21 @@ // Core types for the LiteLLM OpenCode plugin

chatApiModels?: string[]
/**
* Arbitrary HTTP headers to include in every request to the LiteLLM
* proxy during model discovery (health check + `/v1/models`).
*
* Useful for proxies behind Cloudflare Access or other gateways that
* require extra authentication headers beyond the standard
* `Authorization: Bearer` token.
*
* Example (Cloudflare Access Service Token):
* ```json
* {
* "customHeaders": {
* "CF-Access-Client-Id": "{env:CF_ACCESS_CLIENT_ID}",
* "CF-Access-Client-Secret": "{env:CF_ACCESS_CLIENT_SECRET}"
* }
* }
* ```
*/
customHeaders?: Record<string, string>
}

@@ -24,3 +24,3 @@ import type { LiteLLMModel, LiteLLMModelsResponse } from '../types'

function buildHeaders(apiKey?: string): Record<string, string> {
function buildHeaders(apiKey?: string, customHeaders?: Record<string, string>): Record<string, string> {
const headers: Record<string, string> = {

@@ -33,2 +33,5 @@ 'Content-Type': 'application/json',

}
if (customHeaders) {
Object.assign(headers, customHeaders)
}
return headers

@@ -41,2 +44,3 @@ }

apiKey?: string,
customHeaders?: Record<string, string>,
): Promise<boolean> {

@@ -46,3 +50,3 @@ try {

method: 'GET',
headers: buildHeaders(apiKey),
headers: buildHeaders(apiKey, customHeaders),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),

@@ -63,2 +67,3 @@ })

apiKey?: string,
customHeaders?: Record<string, string>,
): Promise<LiteLLMModel[]> {

@@ -68,3 +73,3 @@ const url = buildAPIURL(baseURL)

method: 'GET',
headers: buildHeaders(apiKey),
headers: buildHeaders(apiKey, customHeaders),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),

@@ -86,7 +91,7 @@ })

*/
export async function autoDetectLiteLLM(apiKey?: string): Promise<string | null> {
export async function autoDetectLiteLLM(apiKey?: string, customHeaders?: Record<string, string>): Promise<string | null> {
const commonPorts = [4000, 8000, 8080]
for (const port of commonPorts) {
const baseURL = `http://localhost:${port}`
if (await checkLiteLLMHealth(baseURL, apiKey)) {
if (await checkLiteLLMHealth(baseURL, apiKey, customHeaders)) {
return baseURL

@@ -93,0 +98,0 @@ }