opencode-provider-litellm 0.3.1 → 0.5.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 +61 -14
- package/package.json +1 -1
- package/src/gcloud-token.test.ts +255 -0
- package/src/gcloud-token.ts +145 -0
- package/src/plugin.test.ts +63 -60
- package/src/plugin.ts +42 -51
- package/src/types.ts +0 -23
- package/src/utils.test.ts +51 -0
- package/src/utils.ts +10 -1
- package/src/skills.test.ts +0 -725
- package/src/skills.ts +0 -375
package/src/skills.test.ts
DELETED
|
@@ -1,725 +0,0 @@
|
|
|
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
|
-
listPublicSkills,
|
|
7
|
-
registerSkill,
|
|
8
|
-
enableSkill,
|
|
9
|
-
disableSkill,
|
|
10
|
-
fetchSkillContent,
|
|
11
|
-
createSkillToolDefinitions,
|
|
12
|
-
createSkillsInjector,
|
|
13
|
-
resetSkillsCache,
|
|
14
|
-
} from './skills.js'
|
|
15
|
-
|
|
16
|
-
const mockSkill: Skill = {
|
|
17
|
-
id: 'skill-1',
|
|
18
|
-
name: 'code-review',
|
|
19
|
-
version: '1.0.0',
|
|
20
|
-
description: 'Reviews code for best practices',
|
|
21
|
-
source: {
|
|
22
|
-
source: 'git-subdir',
|
|
23
|
-
url: 'https://github.com/org/repo',
|
|
24
|
-
path: 'skills/code-review',
|
|
25
|
-
},
|
|
26
|
-
author: null,
|
|
27
|
-
homepage: null,
|
|
28
|
-
keywords: null,
|
|
29
|
-
category: null,
|
|
30
|
-
domain: null,
|
|
31
|
-
namespace: null,
|
|
32
|
-
enabled: true,
|
|
33
|
-
created_at: '2026-01-01T00:00:00Z',
|
|
34
|
-
updated_at: '2026-01-01T00:00:00Z',
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('listSkills', () => {
|
|
38
|
-
const config: PluginConfig = {
|
|
39
|
-
url: 'https://litellm.example.com',
|
|
40
|
-
apiKey: 'test-api-key',
|
|
41
|
-
}
|
|
42
|
-
const token = 'test-token'
|
|
43
|
-
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
vi.restoreAllMocks()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('returns skills from plugins response', async () => {
|
|
49
|
-
const mockPlugins = { plugins: [mockSkill] }
|
|
50
|
-
|
|
51
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
52
|
-
ok: true,
|
|
53
|
-
status: 200,
|
|
54
|
-
json: async () => mockPlugins,
|
|
55
|
-
})
|
|
56
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
57
|
-
|
|
58
|
-
const result = await listSkills(config, token)
|
|
59
|
-
|
|
60
|
-
expect(result).toEqual([mockSkill])
|
|
61
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
62
|
-
'https://litellm.example.com/claude-code/plugins',
|
|
63
|
-
expect.objectContaining({
|
|
64
|
-
method: 'GET',
|
|
65
|
-
headers: expect.objectContaining({
|
|
66
|
-
Authorization: 'Bearer test-token',
|
|
67
|
-
}),
|
|
68
|
-
signal: expect.any(AbortSignal),
|
|
69
|
-
}),
|
|
70
|
-
)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('returns [] on error', async () => {
|
|
74
|
-
const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
|
|
75
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
76
|
-
|
|
77
|
-
const result = await listSkills(config, token)
|
|
78
|
-
expect(result).toEqual([])
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('returns [] on non-ok response', async () => {
|
|
82
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
83
|
-
ok: false,
|
|
84
|
-
status: 500,
|
|
85
|
-
})
|
|
86
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
87
|
-
|
|
88
|
-
const result = await listSkills(config, token)
|
|
89
|
-
expect(result).toEqual([])
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('returns [] on invalid response format', async () => {
|
|
93
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
94
|
-
ok: true,
|
|
95
|
-
status: 200,
|
|
96
|
-
json: async () => { plugins: null },
|
|
97
|
-
})
|
|
98
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
99
|
-
|
|
100
|
-
const result = await listSkills(config, token)
|
|
101
|
-
expect(result).toEqual([])
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('respects timeout (AbortError after 10s)', async () => {
|
|
105
|
-
vi.useFakeTimers()
|
|
106
|
-
|
|
107
|
-
const mockFetch = vi.fn().mockImplementation((_url: string, init: RequestInit) => {
|
|
108
|
-
return new Promise<Response>((_resolve, reject) => {
|
|
109
|
-
const signal = init?.signal
|
|
110
|
-
if (signal) {
|
|
111
|
-
signal.addEventListener('abort', () => {
|
|
112
|
-
reject(new DOMException('The operation was aborted.', 'AbortError'))
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
118
|
-
|
|
119
|
-
const promise = listSkills(config, token)
|
|
120
|
-
|
|
121
|
-
await vi.advanceTimersByTimeAsync(10001)
|
|
122
|
-
|
|
123
|
-
const result = await promise
|
|
124
|
-
|
|
125
|
-
vi.useRealTimers()
|
|
126
|
-
|
|
127
|
-
expect(result).toEqual([])
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('listPublicSkills', () => {
|
|
132
|
-
const config: PluginConfig = {
|
|
133
|
-
url: 'https://litellm.example.com',
|
|
134
|
-
apiKey: 'test-api-key',
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
beforeEach(() => {
|
|
138
|
-
vi.restoreAllMocks()
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('returns public skills without auth', async () => {
|
|
142
|
-
const mockResponse = { plugins: [mockSkill] }
|
|
143
|
-
|
|
144
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
145
|
-
ok: true,
|
|
146
|
-
status: 200,
|
|
147
|
-
json: async () => mockResponse,
|
|
148
|
-
})
|
|
149
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
150
|
-
|
|
151
|
-
const result = await listPublicSkills(config)
|
|
152
|
-
|
|
153
|
-
expect(result).toEqual([mockSkill])
|
|
154
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
155
|
-
'https://litellm.example.com/public/skill_hub',
|
|
156
|
-
expect.objectContaining({
|
|
157
|
-
method: 'GET',
|
|
158
|
-
signal: expect.any(AbortSignal),
|
|
159
|
-
}),
|
|
160
|
-
)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('returns [] on error', async () => {
|
|
164
|
-
const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
|
|
165
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
166
|
-
|
|
167
|
-
const result = await listPublicSkills(config)
|
|
168
|
-
expect(result).toEqual([])
|
|
169
|
-
})
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
describe('registerSkill', () => {
|
|
173
|
-
const config: PluginConfig = {
|
|
174
|
-
url: 'https://litellm.example.com',
|
|
175
|
-
apiKey: 'test-api-key',
|
|
176
|
-
}
|
|
177
|
-
const token = 'test-token'
|
|
178
|
-
|
|
179
|
-
beforeEach(() => {
|
|
180
|
-
vi.restoreAllMocks()
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('returns success message', async () => {
|
|
184
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
185
|
-
ok: true,
|
|
186
|
-
status: 200,
|
|
187
|
-
json: async () => ({
|
|
188
|
-
status: 'success',
|
|
189
|
-
action: 'created',
|
|
190
|
-
plugin: { id: 'new-skill-1', name: 'my-skill' },
|
|
191
|
-
}),
|
|
192
|
-
})
|
|
193
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
194
|
-
|
|
195
|
-
const result = await registerSkill(
|
|
196
|
-
config,
|
|
197
|
-
token,
|
|
198
|
-
'my-skill',
|
|
199
|
-
'https://github.com/org/repo',
|
|
200
|
-
'skills/my-skill',
|
|
201
|
-
'A test skill',
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
expect(result).toBe('Skill "my-skill" registered (id: new-skill-1)')
|
|
205
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
206
|
-
'https://litellm.example.com/claude-code/plugins',
|
|
207
|
-
expect.objectContaining({
|
|
208
|
-
method: 'POST',
|
|
209
|
-
headers: expect.objectContaining({
|
|
210
|
-
Authorization: 'Bearer test-token',
|
|
211
|
-
'Content-Type': 'application/json',
|
|
212
|
-
}),
|
|
213
|
-
body: JSON.stringify({
|
|
214
|
-
name: 'my-skill',
|
|
215
|
-
source: {
|
|
216
|
-
source: 'git-subdir',
|
|
217
|
-
url: 'https://github.com/org/repo',
|
|
218
|
-
path: 'skills/my-skill',
|
|
219
|
-
},
|
|
220
|
-
description: 'A test skill',
|
|
221
|
-
domain: null,
|
|
222
|
-
}),
|
|
223
|
-
signal: expect.any(AbortSignal),
|
|
224
|
-
}),
|
|
225
|
-
)
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('returns error string on failure', async () => {
|
|
229
|
-
const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
|
|
230
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
231
|
-
|
|
232
|
-
const result = await registerSkill(
|
|
233
|
-
config,
|
|
234
|
-
token,
|
|
235
|
-
'my-skill',
|
|
236
|
-
'https://github.com/org/repo',
|
|
237
|
-
'skills/my-skill',
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
expect(result).toBe('Error registering skill: connection refused')
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
it('returns error string on non-ok response', async () => {
|
|
244
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
245
|
-
ok: false,
|
|
246
|
-
status: 400,
|
|
247
|
-
})
|
|
248
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
249
|
-
|
|
250
|
-
const result = await registerSkill(
|
|
251
|
-
config,
|
|
252
|
-
token,
|
|
253
|
-
'my-skill',
|
|
254
|
-
'https://github.com/org/repo',
|
|
255
|
-
'skills/my-skill',
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
expect(result).toContain('Error registering skill')
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it('includes domain when provided', async () => {
|
|
262
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
263
|
-
ok: true,
|
|
264
|
-
status: 200,
|
|
265
|
-
json: async () => ({ plugin: { id: 'skill-3', name: 'domain-skill' } }),
|
|
266
|
-
})
|
|
267
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
268
|
-
|
|
269
|
-
await registerSkill(
|
|
270
|
-
config,
|
|
271
|
-
token,
|
|
272
|
-
'domain-skill',
|
|
273
|
-
'https://github.com/org/repo',
|
|
274
|
-
'skills/domain-skill',
|
|
275
|
-
'A domain skill',
|
|
276
|
-
'Productivity',
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
280
|
-
'https://litellm.example.com/claude-code/plugins',
|
|
281
|
-
expect.objectContaining({
|
|
282
|
-
body: JSON.stringify({
|
|
283
|
-
name: 'domain-skill',
|
|
284
|
-
source: {
|
|
285
|
-
source: 'git-subdir',
|
|
286
|
-
url: 'https://github.com/org/repo',
|
|
287
|
-
path: 'skills/domain-skill',
|
|
288
|
-
},
|
|
289
|
-
description: 'A domain skill',
|
|
290
|
-
domain: 'Productivity',
|
|
291
|
-
}),
|
|
292
|
-
}),
|
|
293
|
-
)
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
describe('enableSkill', () => {
|
|
298
|
-
const config: PluginConfig = {
|
|
299
|
-
url: 'https://litellm.example.com',
|
|
300
|
-
apiKey: 'test-api-key',
|
|
301
|
-
}
|
|
302
|
-
const token = 'test-token'
|
|
303
|
-
|
|
304
|
-
beforeEach(() => {
|
|
305
|
-
vi.restoreAllMocks()
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
it('returns success message', async () => {
|
|
309
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
310
|
-
ok: true,
|
|
311
|
-
status: 200,
|
|
312
|
-
})
|
|
313
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
314
|
-
|
|
315
|
-
const result = await enableSkill(config, token, 'my-skill')
|
|
316
|
-
|
|
317
|
-
expect(result).toBe('Skill "my-skill" enabled')
|
|
318
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
319
|
-
'https://litellm.example.com/claude-code/plugins/my-skill/enable',
|
|
320
|
-
expect.objectContaining({
|
|
321
|
-
method: 'POST',
|
|
322
|
-
headers: expect.objectContaining({
|
|
323
|
-
Authorization: 'Bearer test-token',
|
|
324
|
-
}),
|
|
325
|
-
signal: expect.any(AbortSignal),
|
|
326
|
-
}),
|
|
327
|
-
)
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
it('returns error string on failure', async () => {
|
|
331
|
-
const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
|
|
332
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
333
|
-
|
|
334
|
-
const result = await enableSkill(config, token, 'my-skill')
|
|
335
|
-
|
|
336
|
-
expect(result).toBe('Error enabling skill: connection refused')
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('returns error string on non-ok response', async () => {
|
|
340
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
341
|
-
ok: false,
|
|
342
|
-
status: 404,
|
|
343
|
-
})
|
|
344
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
345
|
-
|
|
346
|
-
const result = await enableSkill(config, token, 'my-skill')
|
|
347
|
-
|
|
348
|
-
expect(result).toContain('Error enabling skill')
|
|
349
|
-
})
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
describe('disableSkill', () => {
|
|
353
|
-
const config: PluginConfig = {
|
|
354
|
-
url: 'https://litellm.example.com',
|
|
355
|
-
apiKey: 'test-api-key',
|
|
356
|
-
}
|
|
357
|
-
const token = 'test-token'
|
|
358
|
-
|
|
359
|
-
beforeEach(() => {
|
|
360
|
-
vi.restoreAllMocks()
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
it('returns success message', async () => {
|
|
364
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
365
|
-
ok: true,
|
|
366
|
-
status: 200,
|
|
367
|
-
})
|
|
368
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
369
|
-
|
|
370
|
-
const result = await disableSkill(config, token, 'my-skill')
|
|
371
|
-
|
|
372
|
-
expect(result).toBe('Skill "my-skill" disabled')
|
|
373
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
374
|
-
'https://litellm.example.com/claude-code/plugins/my-skill/disable',
|
|
375
|
-
expect.objectContaining({
|
|
376
|
-
method: 'POST',
|
|
377
|
-
headers: expect.objectContaining({
|
|
378
|
-
Authorization: 'Bearer test-token',
|
|
379
|
-
}),
|
|
380
|
-
signal: expect.any(AbortSignal),
|
|
381
|
-
}),
|
|
382
|
-
)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it('returns error string on failure', async () => {
|
|
386
|
-
const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
|
|
387
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
388
|
-
|
|
389
|
-
const result = await disableSkill(config, token, 'my-skill')
|
|
390
|
-
|
|
391
|
-
expect(result).toBe('Error disabling skill: connection refused')
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
it('returns error string on non-ok response', async () => {
|
|
395
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
396
|
-
ok: false,
|
|
397
|
-
status: 404,
|
|
398
|
-
})
|
|
399
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
400
|
-
|
|
401
|
-
const result = await disableSkill(config, token, 'my-skill')
|
|
402
|
-
|
|
403
|
-
expect(result).toContain('Error disabling skill')
|
|
404
|
-
})
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
describe('fetchSkillContent', () => {
|
|
408
|
-
beforeEach(() => {
|
|
409
|
-
vi.restoreAllMocks()
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
it('returns SKILL.md content from GitHub', async () => {
|
|
413
|
-
const mockContent = '# Test Skill\n\nThis is a test skill.'
|
|
414
|
-
|
|
415
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
416
|
-
ok: true,
|
|
417
|
-
status: 200,
|
|
418
|
-
text: async () => mockContent,
|
|
419
|
-
})
|
|
420
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
421
|
-
|
|
422
|
-
const result = await fetchSkillContent(mockSkill)
|
|
423
|
-
|
|
424
|
-
expect(result).toBe(mockContent)
|
|
425
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
426
|
-
'https://raw.githubusercontent.com/org/repo/master/skills/code-review/SKILL.md',
|
|
427
|
-
expect.any(Object),
|
|
428
|
-
)
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
it('returns null on non-ok response', async () => {
|
|
432
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
433
|
-
ok: false,
|
|
434
|
-
status: 404,
|
|
435
|
-
})
|
|
436
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
437
|
-
|
|
438
|
-
const result = await fetchSkillContent(mockSkill)
|
|
439
|
-
expect(result).toBeNull()
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
it('returns null on network error', async () => {
|
|
443
|
-
const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
|
|
444
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
445
|
-
|
|
446
|
-
const result = await fetchSkillContent(mockSkill)
|
|
447
|
-
expect(result).toBeNull()
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
it('returns null for unsupported source type', async () => {
|
|
451
|
-
const mockFetch = vi.fn()
|
|
452
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
453
|
-
|
|
454
|
-
const unsupportedSkill: Skill = {
|
|
455
|
-
...mockSkill,
|
|
456
|
-
source: { source: 'inline', url: 'not-a-git-url' },
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const result = await fetchSkillContent(unsupportedSkill)
|
|
460
|
-
expect(result).toBeNull()
|
|
461
|
-
expect(mockFetch).not.toHaveBeenCalled()
|
|
462
|
-
})
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
describe('createSkillToolDefinitions', () => {
|
|
466
|
-
const config: PluginConfig = {
|
|
467
|
-
url: 'https://litellm.example.com',
|
|
468
|
-
apiKey: 'test-api-key',
|
|
469
|
-
}
|
|
470
|
-
const token = 'test-token'
|
|
471
|
-
|
|
472
|
-
beforeEach(() => {
|
|
473
|
-
vi.restoreAllMocks()
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
it('returns 4 tools with correct names', () => {
|
|
477
|
-
const result = createSkillToolDefinitions(config, token)
|
|
478
|
-
|
|
479
|
-
expect(Object.keys(result)).toHaveLength(4)
|
|
480
|
-
expect(result).toHaveProperty('skill_list')
|
|
481
|
-
expect(result).toHaveProperty('skill_register')
|
|
482
|
-
expect(result).toHaveProperty('skill_enable')
|
|
483
|
-
expect(result).toHaveProperty('skill_disable')
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
it('skill_list has correct description', () => {
|
|
487
|
-
const result = createSkillToolDefinitions(config, token)
|
|
488
|
-
|
|
489
|
-
expect(result.skill_list.description).toBe('List all skills registered on the LiteLLM Skills Gateway')
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
it('skill_register has correct description', () => {
|
|
493
|
-
const result = createSkillToolDefinitions(config, token)
|
|
494
|
-
|
|
495
|
-
expect(result.skill_register.description).toBe('Register a new skill on the LiteLLM Skills Gateway pointing to a git source')
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
it('skill_enable has correct description', () => {
|
|
499
|
-
const result = createSkillToolDefinitions(config, token)
|
|
500
|
-
|
|
501
|
-
expect(result.skill_enable.description).toBe('Enable (publish) a skill on the LiteLLM Skills Gateway')
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
it('skill_disable has correct description', () => {
|
|
505
|
-
const result = createSkillToolDefinitions(config, token)
|
|
506
|
-
|
|
507
|
-
expect(result.skill_disable.description).toBe('Disable (unpublish) a skill on the LiteLLM Skills Gateway')
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
it('skill_list execute returns formatted markdown table', async () => {
|
|
511
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
512
|
-
ok: true,
|
|
513
|
-
status: 200,
|
|
514
|
-
json: async () => ({
|
|
515
|
-
plugins: [
|
|
516
|
-
{ ...mockSkill, name: 'code-review', description: 'Reviews code', enabled: true },
|
|
517
|
-
{ ...mockSkill, name: 'security-scan', description: 'Scans security', enabled: false },
|
|
518
|
-
],
|
|
519
|
-
}),
|
|
520
|
-
})
|
|
521
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
522
|
-
|
|
523
|
-
const result = createSkillToolDefinitions(config, token)
|
|
524
|
-
|
|
525
|
-
const output = await result.skill_list.execute({}, {} as any)
|
|
526
|
-
|
|
527
|
-
expect(output).toContain('code-review')
|
|
528
|
-
expect(output).toContain('security-scan')
|
|
529
|
-
expect(output).toContain('Reviews code')
|
|
530
|
-
expect(output).toContain('Scans security')
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
it('skill_register execute calls registerSkill', async () => {
|
|
534
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
535
|
-
ok: true,
|
|
536
|
-
status: 200,
|
|
537
|
-
json: async () => ({ plugin: { id: 'new-skill', name: 'new-skill' } }),
|
|
538
|
-
})
|
|
539
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
540
|
-
|
|
541
|
-
const result = createSkillToolDefinitions(config, token)
|
|
542
|
-
|
|
543
|
-
const output = await result.skill_register.execute(
|
|
544
|
-
{
|
|
545
|
-
name: 'new-skill',
|
|
546
|
-
git_url: 'https://github.com/org/repo',
|
|
547
|
-
git_path: 'skills/new-skill',
|
|
548
|
-
description: 'A new skill',
|
|
549
|
-
},
|
|
550
|
-
{} as any,
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
expect(output).toBe('Skill "new-skill" registered (id: new-skill)')
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
it('skill_enable execute calls enableSkill', async () => {
|
|
557
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
558
|
-
ok: true,
|
|
559
|
-
status: 200,
|
|
560
|
-
})
|
|
561
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
562
|
-
|
|
563
|
-
const result = createSkillToolDefinitions(config, token)
|
|
564
|
-
|
|
565
|
-
const output = await result.skill_enable.execute(
|
|
566
|
-
{ name: 'skill-to-enable' },
|
|
567
|
-
{} as any,
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
expect(output).toBe('Skill "skill-to-enable" enabled')
|
|
571
|
-
})
|
|
572
|
-
|
|
573
|
-
it('skill_disable execute calls disableSkill', async () => {
|
|
574
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
575
|
-
ok: true,
|
|
576
|
-
status: 200,
|
|
577
|
-
})
|
|
578
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
579
|
-
|
|
580
|
-
const result = createSkillToolDefinitions(config, token)
|
|
581
|
-
|
|
582
|
-
const output = await result.skill_disable.execute(
|
|
583
|
-
{ name: 'skill-to-disable' },
|
|
584
|
-
{} as any,
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
expect(output).toBe('Skill "skill-to-disable" disabled')
|
|
588
|
-
})
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
describe('createSkillsInjector', () => {
|
|
592
|
-
const config: PluginConfig = {
|
|
593
|
-
url: 'https://litellm.example.com',
|
|
594
|
-
apiKey: 'test-api-key',
|
|
595
|
-
}
|
|
596
|
-
const token = 'test-token'
|
|
597
|
-
|
|
598
|
-
beforeEach(() => {
|
|
599
|
-
vi.restoreAllMocks()
|
|
600
|
-
resetSkillsCache()
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
it('injects skills as text parts', async () => {
|
|
604
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
605
|
-
ok: true,
|
|
606
|
-
status: 200,
|
|
607
|
-
json: async () => ({
|
|
608
|
-
plugins: [
|
|
609
|
-
{ ...mockSkill, name: 'code-review', description: 'Reviews code', enabled: true },
|
|
610
|
-
{ ...mockSkill, name: 'security-scan', description: 'Scans security', enabled: true },
|
|
611
|
-
],
|
|
612
|
-
}),
|
|
613
|
-
})
|
|
614
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
615
|
-
|
|
616
|
-
const injector = createSkillsInjector(config, token)
|
|
617
|
-
|
|
618
|
-
const input = { sessionID: 'main-session' }
|
|
619
|
-
const output: { message: any; parts: Array<{ type: string; text: string }> } = { message: { content: 'Hello' }, parts: [] }
|
|
620
|
-
|
|
621
|
-
await injector(input, output)
|
|
622
|
-
|
|
623
|
-
expect(output.parts).toHaveLength(1)
|
|
624
|
-
expect(output.parts[0].type).toBe('text')
|
|
625
|
-
expect(output.parts[0].text).toContain('<skill name="code-review">Reviews code</skill>')
|
|
626
|
-
expect(output.parts[0].text).toContain('<skill name="security-scan">Scans security</skill>')
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
it('skips ALL sub-agent sessions (returns when input.agent is truthy)', async () => {
|
|
630
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
631
|
-
ok: true,
|
|
632
|
-
status: 200,
|
|
633
|
-
json: async () => ({ plugins: [mockSkill] }),
|
|
634
|
-
})
|
|
635
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
636
|
-
|
|
637
|
-
const injector = createSkillsInjector(config, token)
|
|
638
|
-
|
|
639
|
-
const input = { sessionID: 'main-session', agent: 'sub-agent-1' }
|
|
640
|
-
const output = { message: { content: 'Hello' }, parts: [] }
|
|
641
|
-
|
|
642
|
-
await injector(input, output)
|
|
643
|
-
|
|
644
|
-
expect(output.parts).toEqual([])
|
|
645
|
-
expect(mockFetch).not.toHaveBeenCalled()
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
it('skips when no enabled skills', async () => {
|
|
649
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
650
|
-
ok: true,
|
|
651
|
-
status: 200,
|
|
652
|
-
json: async () => ({
|
|
653
|
-
plugins: [
|
|
654
|
-
{ ...mockSkill, name: 'disabled-skill', description: 'Disabled', enabled: false },
|
|
655
|
-
],
|
|
656
|
-
}),
|
|
657
|
-
})
|
|
658
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
659
|
-
|
|
660
|
-
const injector = createSkillsInjector(config, token)
|
|
661
|
-
|
|
662
|
-
const input = { sessionID: 'main-session' }
|
|
663
|
-
const output = { message: { content: 'Hello' }, parts: [] }
|
|
664
|
-
|
|
665
|
-
await injector(input, output)
|
|
666
|
-
|
|
667
|
-
expect(output.parts).toEqual([])
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
it('uses "No description" for null descriptions', async () => {
|
|
671
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
672
|
-
ok: true,
|
|
673
|
-
status: 200,
|
|
674
|
-
json: async () => ({
|
|
675
|
-
plugins: [
|
|
676
|
-
{ ...mockSkill, name: 'no-desc-skill', description: null, enabled: true },
|
|
677
|
-
],
|
|
678
|
-
}),
|
|
679
|
-
})
|
|
680
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
681
|
-
|
|
682
|
-
const injector = createSkillsInjector(config, token)
|
|
683
|
-
|
|
684
|
-
const input = { sessionID: 'main-session' }
|
|
685
|
-
const output = { message: { content: 'Hello' }, parts: [] }
|
|
686
|
-
|
|
687
|
-
await injector(input, output)
|
|
688
|
-
|
|
689
|
-
expect(output.parts[0].text).toContain('<skill name="no-desc-skill">No description</skill>')
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
it('cache TTL works (second call within TTL uses cache)', async () => {
|
|
693
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
694
|
-
ok: true,
|
|
695
|
-
status: 200,
|
|
696
|
-
json: async () => ({
|
|
697
|
-
plugins: [
|
|
698
|
-
{ ...mockSkill, name: 'cached-skill', description: 'Cached', enabled: true },
|
|
699
|
-
],
|
|
700
|
-
}),
|
|
701
|
-
})
|
|
702
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
703
|
-
|
|
704
|
-
const injector = createSkillsInjector(config, token)
|
|
705
|
-
|
|
706
|
-
await injector({ sessionID: 'session-1' }, { message: {}, parts: [] })
|
|
707
|
-
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
708
|
-
|
|
709
|
-
await injector({ sessionID: 'session-2' }, { message: {}, parts: [] })
|
|
710
|
-
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
it('silently skips on fetch failure', async () => {
|
|
714
|
-
const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
|
|
715
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
716
|
-
|
|
717
|
-
const injector = createSkillsInjector(config, token)
|
|
718
|
-
|
|
719
|
-
const input = { sessionID: 'main-session' }
|
|
720
|
-
const output = { message: { content: 'Hello' }, parts: [] }
|
|
721
|
-
|
|
722
|
-
await expect(injector(input, output)).resolves.toBeUndefined()
|
|
723
|
-
expect(output.parts).toEqual([])
|
|
724
|
-
})
|
|
725
|
-
})
|