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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # opencode-provider-litellm
2
2
 
3
- OpenCode plugin that auto-discovers models from any [LiteLLM](https://github.com/BerriAI/litellm) proxy — complete with costs, context limits, capabilities, and auth. Zero config, zero hand-maintained model lists.
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's model picker automatically.
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 two OpenCode hooks:
90
+ The plugin uses three OpenCode hooks:
62
91
 
63
92
  | Hook | Purpose |
64
93
  |------|---------|
65
- | `config` | Queries `/health` + `/model/info` on startup, discovers models with rich metadata (costs, limits, capabilities), and injects them into OpenCode |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-provider-litellm",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "OpenCode plugin for any LiteLLM proxy — auto-discovers models, auth, and capabilities",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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('tool hook passes correct config and apiKey to createSkillToolDefinitions', async () => {
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
- return
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