opencode-provider-litellm 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -11
- package/package.json +1 -1
- package/src/plugin.test.ts +1 -61
- package/src/plugin.ts +16 -46
- package/src/types.ts +0 -23
- package/src/skills.test.ts +0 -725
- package/src/skills.ts +0 -375
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# opencode-provider-litellm
|
|
2
2
|
|
|
3
|
-
OpenCode plugin
|
|
3
|
+
OpenCode plugin for any [LiteLLM](https://github.com/BerriAI/litellm) proxy — auto-discovers models, MCP tools, and auth. Zero config, zero hand-maintained model lists.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ opencode plugin opencode-provider-litellm
|
|
|
15
15
|
# 3. Restart OpenCode
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
All models from your LiteLLM proxy appear in OpenCode
|
|
18
|
+
All models and MCP tools from your LiteLLM proxy appear in OpenCode automatically.
|
|
19
19
|
|
|
20
20
|
## Configuration
|
|
21
21
|
|
|
@@ -56,21 +56,44 @@ You can also authenticate interactively via the OpenCode TUI:
|
|
|
56
56
|
|
|
57
57
|
The key is stored in OpenCode's auth store.
|
|
58
58
|
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
### Model discovery
|
|
62
|
+
|
|
63
|
+
Queries LiteLLM on startup and injects all models with rich metadata into OpenCode:
|
|
64
|
+
|
|
65
|
+
- `/health` — model list with internal UUIDs
|
|
66
|
+
- `/model/info?litellm_model_id={uuid}` — costs, context limits, vision, tool calling, reasoning, etc.
|
|
67
|
+
|
|
68
|
+
Custom `model_info` updates via `/model/update` are respected — no hardcoded fallbacks.
|
|
69
|
+
|
|
70
|
+
### MCP tools
|
|
71
|
+
|
|
72
|
+
Discovers tools registered on LiteLLM's MCP servers at startup and exposes them as native OpenCode tools. Each tool keeps its original description and parameter schema.
|
|
73
|
+
|
|
74
|
+
### Skills
|
|
75
|
+
|
|
76
|
+
Skills registered in LiteLLM's [Skills Gateway](https://docs.litellm.ai/docs/skills_gateway) are made available to OpenCode via the [proxy-sidecar](../llm-server/proxy-sidecar/), which serves skills in OpenCode's native format. Add the sidecar URL to your config:
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
{
|
|
80
|
+
"skills": {
|
|
81
|
+
"urls": ["https://your-litellm-proxy.example.com/opencode/skills"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Skills appear in OpenCode's `/skills` menu and are loaded natively by the agent.
|
|
87
|
+
|
|
59
88
|
## How it works
|
|
60
89
|
|
|
61
|
-
The plugin uses
|
|
90
|
+
The plugin uses three OpenCode hooks:
|
|
62
91
|
|
|
63
92
|
| Hook | Purpose |
|
|
64
93
|
|------|---------|
|
|
65
|
-
| `config` |
|
|
94
|
+
| `config` | Discovers models from LiteLLM and injects them into OpenCode |
|
|
66
95
|
| `auth` | Provides a `/connect` entry point for pasting an API key |
|
|
67
|
-
|
|
68
|
-
Model metadata is fetched from LiteLLM's admin API:
|
|
69
|
-
|
|
70
|
-
- `/health` — model list with internal UUIDs
|
|
71
|
-
- `/model/info?litellm_model_id={uuid}` — rich metadata per model (costs, context limits, vision, tool calling, reasoning, etc.)
|
|
72
|
-
|
|
73
|
-
Custom model_info updates via `/model/update` are respected — no hardcoded fallbacks.
|
|
96
|
+
| `tool` | Exposes discovered MCP tools as native OpenCode tools |
|
|
74
97
|
|
|
75
98
|
## Troubleshooting
|
|
76
99
|
|
|
@@ -79,6 +102,7 @@ Custom model_info updates via `/model/update` are respected — no hardcoded fal
|
|
|
79
102
|
| "Plugin config error" | Set `LITELLM_URL` and `LITELLM_KEY`, or add `url`/`apiKey` to plugin options |
|
|
80
103
|
| "Access denied" (403) | Verify the API key has access to the LiteLLM proxy |
|
|
81
104
|
| "No models discovered" | Check that the proxy is reachable and the `/health` endpoint responds |
|
|
105
|
+
| Skills not showing | Verify the proxy-sidecar is running and the skills URL is in `opencode.json` |
|
|
82
106
|
|
|
83
107
|
## Development
|
|
84
108
|
|
package/package.json
CHANGED
package/src/plugin.test.ts
CHANGED
|
@@ -20,17 +20,10 @@ vi.mock('./mcp-tools.js', () => ({
|
|
|
20
20
|
createMcpToolDefinitions: vi.fn(),
|
|
21
21
|
}))
|
|
22
22
|
|
|
23
|
-
// Mock the Skills module
|
|
24
|
-
vi.mock('./skills.js', () => ({
|
|
25
|
-
createSkillToolDefinitions: vi.fn(),
|
|
26
|
-
createSkillsInjector: vi.fn(),
|
|
27
|
-
}))
|
|
28
|
-
|
|
29
23
|
import { LiteLLMPlugin } from './plugin.js'
|
|
30
24
|
import { discoverModels, injectModelsIntoConfig } from './discovery.js'
|
|
31
25
|
import { resolvePluginConfig } from './utils.js'
|
|
32
26
|
import { createMcpToolDefinitions } from './mcp-tools.js'
|
|
33
|
-
import { createSkillToolDefinitions, createSkillsInjector } from './skills.js'
|
|
34
27
|
|
|
35
28
|
function createMockInput(): PluginInput {
|
|
36
29
|
const logFn = vi.fn().mockResolvedValue(true)
|
|
@@ -79,14 +72,6 @@ describe('LiteLLMPlugin', () => {
|
|
|
79
72
|
vi.mocked(createMcpToolDefinitions).mockResolvedValue({
|
|
80
73
|
mcp_test_server_test_tool: 'mock-mcp-tool',
|
|
81
74
|
})
|
|
82
|
-
// Default Skills mock: returns two tools
|
|
83
|
-
vi.mocked(createSkillToolDefinitions).mockReturnValue({
|
|
84
|
-
skill_list: 'mock-skill-list',
|
|
85
|
-
skill_create: 'mock-skill-create',
|
|
86
|
-
skill_delete: 'mock-skill-delete',
|
|
87
|
-
})
|
|
88
|
-
// Default Skills injector mock
|
|
89
|
-
vi.mocked(createSkillsInjector).mockReturnValue(vi.fn())
|
|
90
75
|
})
|
|
91
76
|
|
|
92
77
|
it('throws on missing config', async () => {
|
|
@@ -212,9 +197,6 @@ describe('LiteLLMPlugin', () => {
|
|
|
212
197
|
|
|
213
198
|
expect(hooks.tool).toBeDefined()
|
|
214
199
|
expect(hooks.tool).toHaveProperty('mcp_test_server_test_tool')
|
|
215
|
-
expect(hooks.tool).toHaveProperty('skill_list')
|
|
216
|
-
expect(hooks.tool).toHaveProperty('skill_create')
|
|
217
|
-
expect(hooks.tool).toHaveProperty('skill_delete')
|
|
218
200
|
})
|
|
219
201
|
|
|
220
202
|
it('tool hook passes correct config and apiKey to createMcpToolDefinitions', async () => {
|
|
@@ -229,41 +211,7 @@ describe('LiteLLMPlugin', () => {
|
|
|
229
211
|
)
|
|
230
212
|
})
|
|
231
213
|
|
|
232
|
-
it('
|
|
233
|
-
await LiteLLMPlugin(mockInput, {
|
|
234
|
-
url: 'https://litellm.example.com',
|
|
235
|
-
apiKey: 'test-api-key',
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
expect(createSkillToolDefinitions).toHaveBeenCalledWith(
|
|
239
|
-
{ url: 'https://litellm.example.com', apiKey: 'test-api-key' },
|
|
240
|
-
'test-api-key',
|
|
241
|
-
)
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('chat.message hook is defined', async () => {
|
|
245
|
-
const hooks = await LiteLLMPlugin(mockInput, {
|
|
246
|
-
url: 'https://litellm.example.com',
|
|
247
|
-
apiKey: 'test-api-key',
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
expect(hooks['chat.message']).toBeDefined()
|
|
251
|
-
expect(typeof hooks['chat.message']).toBe('function')
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('chat.message hook is created with correct config and apiKey', async () => {
|
|
255
|
-
await LiteLLMPlugin(mockInput, {
|
|
256
|
-
url: 'https://litellm.example.com',
|
|
257
|
-
apiKey: 'test-api-key',
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
expect(createSkillsInjector).toHaveBeenCalledWith(
|
|
261
|
-
{ url: 'https://litellm.example.com', apiKey: 'test-api-key' },
|
|
262
|
-
'test-api-key',
|
|
263
|
-
)
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
it('MCP discovery failure does not break the plugin — Skills tools still present', async () => {
|
|
214
|
+
it('MCP discovery failure does not break the plugin', async () => {
|
|
267
215
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
268
216
|
vi.mocked(createMcpToolDefinitions).mockRejectedValue(new Error('MCP server unavailable'))
|
|
269
217
|
|
|
@@ -278,10 +226,6 @@ describe('LiteLLMPlugin', () => {
|
|
|
278
226
|
|
|
279
227
|
// Skills tools should still be present
|
|
280
228
|
expect(hooks.tool).toBeDefined()
|
|
281
|
-
expect(hooks.tool).toHaveProperty('skill_list')
|
|
282
|
-
expect(hooks.tool).toHaveProperty('skill_create')
|
|
283
|
-
expect(hooks.tool).toHaveProperty('skill_delete')
|
|
284
|
-
|
|
285
229
|
// MCP tools should not be present (empty object merged)
|
|
286
230
|
expect(hooks.tool).not.toHaveProperty('mcp_test_server_test_tool')
|
|
287
231
|
|
|
@@ -300,10 +244,6 @@ describe('LiteLLMPlugin', () => {
|
|
|
300
244
|
const hooks = await LiteLLMPlugin(mockInput, undefined)
|
|
301
245
|
|
|
302
246
|
expect(hooks.tool).toBeDefined()
|
|
303
|
-
expect(hooks.tool).toHaveProperty('skill_list')
|
|
304
|
-
expect(hooks['chat.message']).toBeDefined()
|
|
305
|
-
expect(typeof hooks['chat.message']).toBe('function')
|
|
306
|
-
|
|
307
247
|
expect(createMcpToolDefinitions).toHaveBeenCalledWith(
|
|
308
248
|
{ url: 'https://env.litellm.example.com', apiKey: 'env-api-key' },
|
|
309
249
|
'env-api-key',
|
package/src/plugin.ts
CHANGED
|
@@ -2,17 +2,7 @@ import type { Plugin, PluginInput, PluginOptions } from '@opencode-ai/plugin'
|
|
|
2
2
|
import { resolvePluginConfig, getProviderId } from './utils.js'
|
|
3
3
|
import { discoverModels, injectModelsIntoConfig } from './discovery.js'
|
|
4
4
|
import { createMcpToolDefinitions } from './mcp-tools.js'
|
|
5
|
-
import { createSkillToolDefinitions, createSkillsInjector } from './skills.js'
|
|
6
5
|
|
|
7
|
-
/**
|
|
8
|
-
* Main plugin entry point for the LiteLLM provider.
|
|
9
|
-
*
|
|
10
|
-
* Wires together config (model discovery), auth (/connect API key flow),
|
|
11
|
-
* and chat.headers (per-request Bearer token injection).
|
|
12
|
-
*
|
|
13
|
-
* Auth: LITELLM_URL / LITELLM_KEY env vars take precedence,
|
|
14
|
-
* with fallback to values in opencode.json plugin options.
|
|
15
|
-
*/
|
|
16
6
|
export const LiteLLMPlugin: Plugin = async (
|
|
17
7
|
input: PluginInput,
|
|
18
8
|
options?: PluginOptions,
|
|
@@ -27,7 +17,6 @@ export const LiteLLMPlugin: Plugin = async (
|
|
|
27
17
|
|
|
28
18
|
const providerId = getProviderId()
|
|
29
19
|
|
|
30
|
-
// Discover MCP tools with graceful error handling
|
|
31
20
|
let mcpTools: Record<string, any> = {}
|
|
32
21
|
try {
|
|
33
22
|
mcpTools = await createMcpToolDefinitions(pluginConfig, pluginConfig.apiKey)
|
|
@@ -36,11 +25,7 @@ export const LiteLLMPlugin: Plugin = async (
|
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
return {
|
|
39
|
-
|
|
40
|
-
* Config hook — discovers models from the LiteLLM proxy and injects
|
|
41
|
-
* them into the OpenCode config under the provider.
|
|
42
|
-
*/
|
|
43
|
-
config: async (config) => {
|
|
28
|
+
config: async (config: Record<string, any>) => {
|
|
44
29
|
try {
|
|
45
30
|
const models = await discoverModels(
|
|
46
31
|
pluginConfig,
|
|
@@ -55,23 +40,22 @@ export const LiteLLMPlugin: Plugin = async (
|
|
|
55
40
|
message: 'No models discovered',
|
|
56
41
|
},
|
|
57
42
|
})
|
|
58
|
-
|
|
43
|
+
} else {
|
|
44
|
+
injectModelsIntoConfig(
|
|
45
|
+
config as Parameters<typeof injectModelsIntoConfig>[0],
|
|
46
|
+
providerId,
|
|
47
|
+
pluginConfig.url,
|
|
48
|
+
pluginConfig.apiKey,
|
|
49
|
+
models,
|
|
50
|
+
)
|
|
51
|
+
await input.client.app.log({
|
|
52
|
+
body: {
|
|
53
|
+
service: providerId,
|
|
54
|
+
level: 'info',
|
|
55
|
+
message: `Discovered ${Object.keys(models).length} models`,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
injectModelsIntoConfig(
|
|
62
|
-
config as Parameters<typeof injectModelsIntoConfig>[0],
|
|
63
|
-
providerId,
|
|
64
|
-
pluginConfig.url,
|
|
65
|
-
pluginConfig.apiKey,
|
|
66
|
-
models,
|
|
67
|
-
)
|
|
68
|
-
await input.client.app.log({
|
|
69
|
-
body: {
|
|
70
|
-
service: providerId,
|
|
71
|
-
level: 'info',
|
|
72
|
-
message: `Discovered ${Object.keys(models).length} models`,
|
|
73
|
-
},
|
|
74
|
-
})
|
|
75
59
|
} catch (error) {
|
|
76
60
|
await input.client.app.log({
|
|
77
61
|
body: {
|
|
@@ -83,10 +67,6 @@ export const LiteLLMPlugin: Plugin = async (
|
|
|
83
67
|
}
|
|
84
68
|
},
|
|
85
69
|
|
|
86
|
-
/**
|
|
87
|
-
* Auth hook — lets the user paste an API key via the /connect flow.
|
|
88
|
-
* The key is stored in OpenCode's auth store and used as the Bearer token.
|
|
89
|
-
*/
|
|
90
70
|
auth: {
|
|
91
71
|
provider: providerId,
|
|
92
72
|
methods: [
|
|
@@ -111,18 +91,8 @@ export const LiteLLMPlugin: Plugin = async (
|
|
|
111
91
|
],
|
|
112
92
|
},
|
|
113
93
|
|
|
114
|
-
/**
|
|
115
|
-
* Tool hook — merges dynamically-discovered MCP tools with static
|
|
116
|
-
* Skills CRUD tools.
|
|
117
|
-
*/
|
|
118
94
|
tool: {
|
|
119
95
|
...mcpTools,
|
|
120
|
-
...createSkillToolDefinitions(pluginConfig, pluginConfig.apiKey),
|
|
121
96
|
},
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Chat message hook — injects active Skills as context into chat messages.
|
|
125
|
-
*/
|
|
126
|
-
"chat.message": createSkillsInjector(pluginConfig, pluginConfig.apiKey),
|
|
127
97
|
}
|
|
128
98
|
}
|
package/src/types.ts
CHANGED
|
@@ -15,29 +15,6 @@ export interface McpTool {
|
|
|
15
15
|
input_schema: Record<string, unknown>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface SkillSource {
|
|
19
|
-
source: string
|
|
20
|
-
url: string
|
|
21
|
-
path?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Skill {
|
|
25
|
-
id: string
|
|
26
|
-
name: string
|
|
27
|
-
version: string
|
|
28
|
-
description: string | null
|
|
29
|
-
source: SkillSource
|
|
30
|
-
author: string | null
|
|
31
|
-
homepage: string | null
|
|
32
|
-
keywords: string | null
|
|
33
|
-
category: string | null
|
|
34
|
-
domain: string | null
|
|
35
|
-
namespace: string | null
|
|
36
|
-
enabled: boolean
|
|
37
|
-
created_at: string
|
|
38
|
-
updated_at: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
18
|
export interface OpencodeModelConfig {
|
|
42
19
|
name: string
|
|
43
20
|
tool_call?: boolean
|