opencode-provider-litellm 0.1.1 → 0.3.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.
@@ -0,0 +1,449 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { tool } from '@opencode-ai/plugin'
3
+ import type { PluginConfig, Skill } from './types.js'
4
+ import {
5
+ listSkills,
6
+ createSkill,
7
+ deleteSkill,
8
+ createSkillToolDefinitions,
9
+ createSkillsInjector,
10
+ resetSkillsCache,
11
+ } from './skills.js'
12
+
13
+ describe('listSkills', () => {
14
+ const config: PluginConfig = {
15
+ url: 'https://litellm.example.com',
16
+ apiKey: 'test-api-key',
17
+ }
18
+ const token = 'test-token'
19
+
20
+ beforeEach(() => {
21
+ vi.restoreAllMocks()
22
+ })
23
+
24
+ it('returns skills from mock response', async () => {
25
+ const mockSkills: Skill[] = [
26
+ { id: 'skill-1', name: 'code-review', description: 'Reviews code for best practices', enabled: true },
27
+ { id: 'skill-2', name: 'security-scan', description: 'Scans for security issues', enabled: true },
28
+ ]
29
+
30
+ const mockFetch = vi.fn().mockResolvedValue({
31
+ ok: true,
32
+ status: 200,
33
+ json: async () => mockSkills,
34
+ })
35
+ vi.stubGlobal('fetch', mockFetch)
36
+
37
+ const result = await listSkills(config, token)
38
+
39
+ expect(result).toEqual(mockSkills)
40
+ expect(mockFetch).toHaveBeenCalledWith(
41
+ 'https://litellm.example.com/v1/skills',
42
+ expect.objectContaining({
43
+ method: 'GET',
44
+ headers: expect.objectContaining({
45
+ Authorization: 'Bearer test-token',
46
+ }),
47
+ signal: expect.any(AbortSignal),
48
+ }),
49
+ )
50
+ })
51
+
52
+ it('returns [] on error', async () => {
53
+ const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
54
+ vi.stubGlobal('fetch', mockFetch)
55
+
56
+ const result = await listSkills(config, token)
57
+ expect(result).toEqual([])
58
+ })
59
+
60
+ it('returns [] on non-ok response', async () => {
61
+ const mockFetch = vi.fn().mockResolvedValue({
62
+ ok: false,
63
+ status: 500,
64
+ })
65
+ vi.stubGlobal('fetch', mockFetch)
66
+
67
+ const result = await listSkills(config, token)
68
+ expect(result).toEqual([])
69
+ })
70
+
71
+ it('respects timeout (AbortError after 10s)', async () => {
72
+ vi.useFakeTimers()
73
+
74
+ const mockFetch = vi.fn().mockImplementation((_url: string, init: RequestInit) => {
75
+ return new Promise<Response>((_resolve, reject) => {
76
+ const signal = init?.signal
77
+ if (signal) {
78
+ signal.addEventListener('abort', () => {
79
+ reject(new DOMException('The operation was aborted.', 'AbortError'))
80
+ })
81
+ }
82
+ })
83
+ })
84
+ vi.stubGlobal('fetch', mockFetch)
85
+
86
+ const promise = listSkills(config, token)
87
+
88
+ await vi.advanceTimersByTimeAsync(10001)
89
+
90
+ const result = await promise
91
+
92
+ vi.useRealTimers()
93
+
94
+ expect(result).toEqual([])
95
+ })
96
+ })
97
+
98
+ describe('createSkill', () => {
99
+ const config: PluginConfig = {
100
+ url: 'https://litellm.example.com',
101
+ apiKey: 'test-api-key',
102
+ }
103
+ const token = 'test-token'
104
+
105
+ beforeEach(() => {
106
+ vi.restoreAllMocks()
107
+ })
108
+
109
+ it('returns success message', async () => {
110
+ const mockFetch = vi.fn().mockResolvedValue({
111
+ ok: true,
112
+ status: 200,
113
+ json: async () => ({ id: 'skill-new-1', name: 'my-skill' }),
114
+ })
115
+ vi.stubGlobal('fetch', mockFetch)
116
+
117
+ const result = await createSkill(config, token, 'my-skill', 'A test skill')
118
+
119
+ expect(result).toBe('Skill "my-skill" created (id: skill-new-1)')
120
+ expect(mockFetch).toHaveBeenCalledWith(
121
+ 'https://litellm.example.com/v1/skills',
122
+ expect.objectContaining({
123
+ method: 'POST',
124
+ headers: expect.objectContaining({
125
+ Authorization: 'Bearer test-token',
126
+ 'Content-Type': 'application/json',
127
+ }),
128
+ body: JSON.stringify({ name: 'my-skill', description: 'A test skill', input_schema: undefined, code: undefined }),
129
+ signal: expect.any(AbortSignal),
130
+ }),
131
+ )
132
+ })
133
+
134
+ it('returns error string on failure', async () => {
135
+ const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
136
+ vi.stubGlobal('fetch', mockFetch)
137
+
138
+ const result = await createSkill(config, token, 'my-skill', 'A test skill')
139
+
140
+ expect(result).toBe('Error creating skill: connection refused')
141
+ })
142
+
143
+ it('returns error string on non-ok response', async () => {
144
+ const mockFetch = vi.fn().mockResolvedValue({
145
+ ok: false,
146
+ status: 400,
147
+ })
148
+ vi.stubGlobal('fetch', mockFetch)
149
+
150
+ const result = await createSkill(config, token, 'my-skill', 'A test skill')
151
+
152
+ expect(result).toContain('Error creating skill')
153
+ })
154
+
155
+ it('includes input_schema and code when provided', async () => {
156
+ const mockFetch = vi.fn().mockResolvedValue({
157
+ ok: true,
158
+ status: 200,
159
+ json: async () => ({ id: 'skill-3', name: 'complex-skill' }),
160
+ })
161
+ vi.stubGlobal('fetch', mockFetch)
162
+
163
+ await createSkill(
164
+ config,
165
+ token,
166
+ 'complex-skill',
167
+ 'A complex skill',
168
+ { type: 'object', properties: { value: { type: 'string' } } },
169
+ 'print("hello")',
170
+ )
171
+
172
+ expect(mockFetch).toHaveBeenCalledWith(
173
+ 'https://litellm.example.com/v1/skills',
174
+ expect.objectContaining({
175
+ body: JSON.stringify({
176
+ name: 'complex-skill',
177
+ description: 'A complex skill',
178
+ input_schema: { type: 'object', properties: { value: { type: 'string' } } },
179
+ code: 'print("hello")',
180
+ }),
181
+ }),
182
+ )
183
+ })
184
+ })
185
+
186
+ describe('deleteSkill', () => {
187
+ const config: PluginConfig = {
188
+ url: 'https://litellm.example.com',
189
+ apiKey: 'test-api-key',
190
+ }
191
+ const token = 'test-token'
192
+
193
+ beforeEach(() => {
194
+ vi.restoreAllMocks()
195
+ })
196
+
197
+ it('returns success message', async () => {
198
+ const mockFetch = vi.fn().mockResolvedValue({
199
+ ok: true,
200
+ status: 200,
201
+ })
202
+ vi.stubGlobal('fetch', mockFetch)
203
+
204
+ const result = await deleteSkill(config, token, 'skill-1')
205
+
206
+ expect(result).toBe('Skill "skill-1" deleted')
207
+ expect(mockFetch).toHaveBeenCalledWith(
208
+ 'https://litellm.example.com/v1/skills/skill-1',
209
+ expect.objectContaining({
210
+ method: 'DELETE',
211
+ headers: expect.objectContaining({
212
+ Authorization: 'Bearer test-token',
213
+ }),
214
+ signal: expect.any(AbortSignal),
215
+ }),
216
+ )
217
+ })
218
+
219
+ it('returns error string on failure', async () => {
220
+ const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
221
+ vi.stubGlobal('fetch', mockFetch)
222
+
223
+ const result = await deleteSkill(config, token, 'skill-1')
224
+
225
+ expect(result).toBe('Error deleting skill: connection refused')
226
+ })
227
+
228
+ it('returns error string on non-ok response', async () => {
229
+ const mockFetch = vi.fn().mockResolvedValue({
230
+ ok: false,
231
+ status: 404,
232
+ })
233
+ vi.stubGlobal('fetch', mockFetch)
234
+
235
+ const result = await deleteSkill(config, token, 'skill-1')
236
+
237
+ expect(result).toContain('Error deleting skill')
238
+ })
239
+ })
240
+
241
+ describe('createSkillToolDefinitions', () => {
242
+ const config: PluginConfig = {
243
+ url: 'https://litellm.example.com',
244
+ apiKey: 'test-api-key',
245
+ }
246
+ const token = 'test-token'
247
+
248
+ beforeEach(() => {
249
+ vi.restoreAllMocks()
250
+ })
251
+
252
+ it('returns 3 tools with correct names', () => {
253
+ const result = createSkillToolDefinitions(config, token)
254
+
255
+ expect(Object.keys(result)).toHaveLength(3)
256
+ expect(result).toHaveProperty('skill_list')
257
+ expect(result).toHaveProperty('skill_create')
258
+ expect(result).toHaveProperty('skill_delete')
259
+ })
260
+
261
+ it('skill_list has correct description', () => {
262
+ const result = createSkillToolDefinitions(config, token)
263
+
264
+ expect(result.skill_list.description).toBe('List all skills registered on the LiteLLM proxy')
265
+ })
266
+
267
+ it('skill_create has correct description', () => {
268
+ const result = createSkillToolDefinitions(config, token)
269
+
270
+ expect(result.skill_create.description).toBe('Create a new skill on the LiteLLM proxy')
271
+ })
272
+
273
+ it('skill_delete has correct description', () => {
274
+ const result = createSkillToolDefinitions(config, token)
275
+
276
+ expect(result.skill_delete.description).toBe('Delete a skill from the LiteLLM proxy')
277
+ })
278
+
279
+ it('skill_list execute returns formatted markdown table', async () => {
280
+ const mockFetch = vi.fn().mockResolvedValue({
281
+ ok: true,
282
+ status: 200,
283
+ json: async () => [
284
+ { id: 'skill-1', name: 'code-review', description: 'Reviews code', enabled: true },
285
+ { id: 'skill-2', name: 'security-scan', description: 'Scans security', enabled: false },
286
+ ],
287
+ })
288
+ vi.stubGlobal('fetch', mockFetch)
289
+
290
+ const result = createSkillToolDefinitions(config, token)
291
+
292
+ const output = await result.skill_list.execute({}, {} as any)
293
+
294
+ expect(output).toContain('code-review')
295
+ expect(output).toContain('security-scan')
296
+ expect(output).toContain('Reviews code')
297
+ expect(output).toContain('Scans security')
298
+ })
299
+
300
+ it('skill_create execute calls createSkill', async () => {
301
+ const mockFetch = vi.fn().mockResolvedValue({
302
+ ok: true,
303
+ status: 200,
304
+ json: async () => ({ id: 'skill-new', name: 'new-skill' }),
305
+ })
306
+ vi.stubGlobal('fetch', mockFetch)
307
+
308
+ const result = createSkillToolDefinitions(config, token)
309
+
310
+ const output = await result.skill_create.execute(
311
+ { name: 'new-skill', description: 'A new skill' },
312
+ {} as any,
313
+ )
314
+
315
+ expect(output).toBe('Skill "new-skill" created (id: skill-new)')
316
+ })
317
+
318
+ it('skill_delete execute calls deleteSkill', async () => {
319
+ const mockFetch = vi.fn().mockResolvedValue({
320
+ ok: true,
321
+ status: 200,
322
+ })
323
+ vi.stubGlobal('fetch', mockFetch)
324
+
325
+ const result = createSkillToolDefinitions(config, token)
326
+
327
+ const output = await result.skill_delete.execute(
328
+ { skill_id: 'skill-to-delete' },
329
+ {} as any,
330
+ )
331
+
332
+ expect(output).toBe('Skill "skill-to-delete" deleted')
333
+ })
334
+ })
335
+
336
+ describe('createSkillsInjector', () => {
337
+ const config: PluginConfig = {
338
+ url: 'https://litellm.example.com',
339
+ apiKey: 'test-api-key',
340
+ }
341
+ const token = 'test-token'
342
+
343
+ beforeEach(() => {
344
+ vi.restoreAllMocks()
345
+ resetSkillsCache()
346
+ })
347
+
348
+ it('injects skills as text parts', async () => {
349
+ const mockFetch = vi.fn().mockResolvedValue({
350
+ ok: true,
351
+ status: 200,
352
+ json: async () => [
353
+ { id: 'skill-1', name: 'code-review', description: 'Reviews code', enabled: true },
354
+ { id: 'skill-2', name: 'security-scan', description: 'Scans security' },
355
+ ],
356
+ })
357
+ vi.stubGlobal('fetch', mockFetch)
358
+
359
+ const injector = createSkillsInjector(config, token)
360
+
361
+ const input = { sessionID: 'main-session' }
362
+ const output: { message: any; parts: Array<{ type: string; text: string }> } = { message: { content: 'Hello' }, parts: [] }
363
+
364
+ await injector(input, output)
365
+
366
+ expect(output.parts).toHaveLength(1)
367
+ expect(output.parts[0].type).toBe('text')
368
+ expect(output.parts[0].text).toContain('<skill name="code-review">Reviews code</skill>')
369
+ expect(output.parts[0].text).toContain('<skill name="security-scan">Scans security</skill>')
370
+ })
371
+
372
+ it('skips ALL sub-agent sessions (returns when input.agent is truthy)', async () => {
373
+ const mockFetch = vi.fn().mockResolvedValue({
374
+ ok: true,
375
+ status: 200,
376
+ json: async () => [
377
+ { id: 'skill-1', name: 'code-review', description: 'Reviews code', enabled: true },
378
+ ],
379
+ })
380
+ vi.stubGlobal('fetch', mockFetch)
381
+
382
+ const injector = createSkillsInjector(config, token)
383
+
384
+ const input = { sessionID: 'main-session', agent: 'sub-agent-1' }
385
+ const output = { message: { content: 'Hello' }, parts: [] }
386
+
387
+ await injector(input, output)
388
+
389
+ // Should not have injected anything — sub-agent sessions are skipped
390
+ expect(output.parts).toEqual([])
391
+ // Should not have called fetch since it returns early
392
+ expect(mockFetch).not.toHaveBeenCalled()
393
+ })
394
+
395
+ it('skips when no enabled skills', async () => {
396
+ const mockFetch = vi.fn().mockResolvedValue({
397
+ ok: true,
398
+ status: 200,
399
+ json: async () => [
400
+ { id: 'skill-1', name: 'disabled-skill', description: 'Disabled', enabled: false },
401
+ ],
402
+ })
403
+ vi.stubGlobal('fetch', mockFetch)
404
+
405
+ const injector = createSkillsInjector(config, token)
406
+
407
+ const input = { sessionID: 'main-session' }
408
+ const output = { message: { content: 'Hello' }, parts: [] }
409
+
410
+ await injector(input, output)
411
+
412
+ expect(output.parts).toEqual([])
413
+ })
414
+
415
+ it('cache TTL works (second call within TTL uses cache)', async () => {
416
+ const mockFetch = vi.fn().mockResolvedValue({
417
+ ok: true,
418
+ status: 200,
419
+ json: async () => [
420
+ { id: 'skill-1', name: 'cached-skill', description: 'Cached', enabled: true },
421
+ ],
422
+ })
423
+ vi.stubGlobal('fetch', mockFetch)
424
+
425
+ const injector = createSkillsInjector(config, token)
426
+
427
+ // First call — should fetch
428
+ await injector({ sessionID: 'session-1' }, { message: {}, parts: [] })
429
+ expect(mockFetch).toHaveBeenCalledTimes(1)
430
+
431
+ // Second call — should use cache (fetch not called again)
432
+ await injector({ sessionID: 'session-2' }, { message: {}, parts: [] })
433
+ expect(mockFetch).toHaveBeenCalledTimes(1)
434
+ })
435
+
436
+ it('silently skips on fetch failure', async () => {
437
+ const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
438
+ vi.stubGlobal('fetch', mockFetch)
439
+
440
+ const injector = createSkillsInjector(config, token)
441
+
442
+ const input = { sessionID: 'main-session' }
443
+ const output = { message: { content: 'Hello' }, parts: [] }
444
+
445
+ // Should not throw
446
+ await expect(injector(input, output)).resolves.toBeUndefined()
447
+ expect(output.parts).toEqual([])
448
+ })
449
+ })
package/src/skills.ts ADDED
@@ -0,0 +1,240 @@
1
+ import { tool } from '@opencode-ai/plugin'
2
+ import type { PluginConfig, Skill } from './types.js'
3
+
4
+ interface CacheEntry<T> {
5
+ data: T
6
+ timestamp: number
7
+ }
8
+
9
+ let skillsCache: CacheEntry<Skill[]> | null = null
10
+ const CACHE_TTL_MS = 60_000
11
+
12
+ /** Reset the skills cache. Used for testing. */
13
+ export function resetSkillsCache(): void {
14
+ skillsCache = null
15
+ }
16
+
17
+ /**
18
+ * Fetches all skills from the LiteLLM proxy.
19
+ * Returns an empty array on any error (network, 4xx, 5xx, parse failure).
20
+ * Uses a 10s timeout via AbortController.
21
+ */
22
+ export async function listSkills(
23
+ config: PluginConfig,
24
+ token: string,
25
+ ): Promise<Skill[]> {
26
+ const controller = new AbortController()
27
+ const timeoutId = setTimeout(() => controller.abort(), 10_000)
28
+
29
+ try {
30
+ const response = await fetch(`${config.url}/v1/skills`, {
31
+ method: 'GET',
32
+ headers: {
33
+ Authorization: `Bearer ${token}`,
34
+ },
35
+ signal: controller.signal,
36
+ })
37
+
38
+ if (!response.ok) {
39
+ return []
40
+ }
41
+
42
+ const body = await response.json()
43
+
44
+ if (!Array.isArray(body)) {
45
+ return []
46
+ }
47
+
48
+ return body as Skill[]
49
+ } catch {
50
+ return []
51
+ } finally {
52
+ clearTimeout(timeoutId)
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Creates a new skill on the LiteLLM proxy.
58
+ * Returns a success message string on success, error string on failure.
59
+ * Uses a 10s timeout via AbortController.
60
+ */
61
+ export async function createSkill(
62
+ config: PluginConfig,
63
+ token: string,
64
+ name: string,
65
+ description: string,
66
+ inputSchema?: Record<string, unknown>,
67
+ code?: string,
68
+ ): Promise<string> {
69
+ const controller = new AbortController()
70
+ const timeoutId = setTimeout(() => controller.abort(), 10_000)
71
+
72
+ try {
73
+ const response = await fetch(`${config.url}/v1/skills`, {
74
+ method: 'POST',
75
+ headers: {
76
+ Authorization: `Bearer ${token}`,
77
+ 'Content-Type': 'application/json',
78
+ },
79
+ body: JSON.stringify({
80
+ name,
81
+ description,
82
+ input_schema: inputSchema,
83
+ code,
84
+ }),
85
+ signal: controller.signal,
86
+ })
87
+
88
+ if (!response.ok) {
89
+ return `Error creating skill: HTTP ${response.status}`
90
+ }
91
+
92
+ const body = await response.json()
93
+ const id = body.id ?? 'unknown'
94
+ return `Skill "${name}" created (id: ${id})`
95
+ } catch (error: unknown) {
96
+ const message = error instanceof Error ? error.message : String(error)
97
+ return `Error creating skill: ${message}`
98
+ } finally {
99
+ clearTimeout(timeoutId)
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Deletes a skill from the LiteLLM proxy.
105
+ * Returns a success message string on success, error string on failure.
106
+ * Uses a 10s timeout via AbortController.
107
+ */
108
+ export async function deleteSkill(
109
+ config: PluginConfig,
110
+ token: string,
111
+ skillId: string,
112
+ ): Promise<string> {
113
+ const controller = new AbortController()
114
+ const timeoutId = setTimeout(() => controller.abort(), 10_000)
115
+
116
+ try {
117
+ const response = await fetch(`${config.url}/v1/skills/${skillId}`, {
118
+ method: 'DELETE',
119
+ headers: {
120
+ Authorization: `Bearer ${token}`,
121
+ },
122
+ signal: controller.signal,
123
+ })
124
+
125
+ if (!response.ok) {
126
+ return `Error deleting skill: HTTP ${response.status}`
127
+ }
128
+
129
+ return `Skill "${skillId}" deleted`
130
+ } catch (error: unknown) {
131
+ const message = error instanceof Error ? error.message : String(error)
132
+ return `Error deleting skill: ${message}`
133
+ } finally {
134
+ clearTimeout(timeoutId)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Creates opencode tool definitions for skill CRUD operations.
140
+ * Returns a static Record with three tools: skill_list, skill_create, skill_delete.
141
+ */
142
+ export function createSkillToolDefinitions(
143
+ config: PluginConfig,
144
+ token: string,
145
+ ): Record<string, any> {
146
+ return {
147
+ skill_list: tool({
148
+ description: 'List all skills registered on the LiteLLM proxy',
149
+ args: {},
150
+ async execute(_args: Record<string, unknown>, _context: unknown): Promise<string> {
151
+ const skills = await listSkills(config, token)
152
+
153
+ if (skills.length === 0) {
154
+ return 'No skills found.'
155
+ }
156
+
157
+ const header = '| ID | Name | Description | Enabled |'
158
+ const sep = '|------|------|-------------|---------|'
159
+ const rows = skills
160
+ .map(
161
+ (s) =>
162
+ `| ${s.id} | ${s.name} | ${s.description} | ${s.enabled !== false ? 'yes' : 'no'} |`,
163
+ )
164
+ .join('\n')
165
+
166
+ return [header, sep, ...rows.split('\n')].join('\n')
167
+ },
168
+ }),
169
+
170
+ skill_create: tool({
171
+ description: 'Create a new skill on the LiteLLM proxy',
172
+ args: {
173
+ name: tool.schema.string().describe('Name of the skill'),
174
+ description: tool.schema.string().describe('Description of the skill'),
175
+ input_schema: tool.schema
176
+ .object({})
177
+ .passthrough()
178
+ .optional()
179
+ .describe('Input schema for the skill'),
180
+ code: tool.schema.string().optional().describe('Code for the skill'),
181
+ },
182
+ async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
183
+ return createSkill(
184
+ config,
185
+ token,
186
+ args.name as string,
187
+ args.description as string,
188
+ args.input_schema as Record<string, unknown> | undefined,
189
+ args.code as string | undefined,
190
+ )
191
+ },
192
+ }),
193
+
194
+ skill_delete: tool({
195
+ description: 'Delete a skill from the LiteLLM proxy',
196
+ args: {
197
+ skill_id: tool.schema.string().describe('ID of the skill to delete'),
198
+ },
199
+ async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
200
+ return deleteSkill(config, token, args.skill_id as string)
201
+ },
202
+ }),
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Creates a chat.message hook that injects active skills as context.
208
+ * Uses in-memory cache with 60s TTL to avoid hammering the API.
209
+ * Only injects for main agent sessions — skips all sub-agents.
210
+ */
211
+ export function createSkillsInjector(
212
+ config: PluginConfig,
213
+ token: string,
214
+ ): (
215
+ input: { sessionID: string; agent?: string; model?: any; messageID?: string; variant?: string },
216
+ output: { message: any; parts: any[] },
217
+ ) => Promise<void> {
218
+ return async (input, output) => {
219
+ // Only inject for main agent session — skip ALL sub-agents
220
+ if (input.agent) return
221
+
222
+ // Fetch skills with simple in-memory cache
223
+ let skills: Skill[] = []
224
+ if (skillsCache && Date.now() - skillsCache.timestamp < CACHE_TTL_MS) {
225
+ skills = skillsCache.data
226
+ } else {
227
+ skills = await listSkills(config, token)
228
+ skillsCache = { data: skills, timestamp: Date.now() }
229
+ }
230
+
231
+ const enabledSkills = skills.filter((s) => s.enabled !== false)
232
+ if (enabledSkills.length === 0) return
233
+
234
+ const context = enabledSkills
235
+ .map((s) => `<skill name="${s.name}">${s.description}</skill>`)
236
+ .join('\n')
237
+
238
+ output.parts.push({ type: 'text', text: context })
239
+ }
240
+ }
package/src/types.ts CHANGED
@@ -8,6 +8,21 @@ export interface LiteLLMModel {
8
8
  max_model_len?: number
9
9
  }
10
10
 
11
+ export interface McpTool {
12
+ name: string
13
+ server_name: string
14
+ description: string
15
+ input_schema: Record<string, unknown>
16
+ }
17
+
18
+ export interface Skill {
19
+ id: string
20
+ name: string
21
+ description: string
22
+ enabled?: boolean
23
+ [key: string]: unknown
24
+ }
25
+
11
26
  export interface OpencodeModelConfig {
12
27
  name: string
13
28
  tool_call?: boolean