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.ts
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
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 Skills Gateway.
|
|
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}/claude-code/plugins`, {
|
|
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 (!body || !Array.isArray(body.plugins)) {
|
|
45
|
-
return []
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return body.plugins as Skill[]
|
|
49
|
-
} catch {
|
|
50
|
-
return []
|
|
51
|
-
} finally {
|
|
52
|
-
clearTimeout(timeoutId)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Fetches only enabled (public) skills from the LiteLLM Skill Hub.
|
|
58
|
-
* No auth required. Useful for discovery without credentials.
|
|
59
|
-
*/
|
|
60
|
-
export async function listPublicSkills(config: PluginConfig): Promise<Skill[]> {
|
|
61
|
-
const controller = new AbortController()
|
|
62
|
-
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const response = await fetch(`${config.url}/public/skill_hub`, {
|
|
66
|
-
method: 'GET',
|
|
67
|
-
signal: controller.signal,
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
if (!response.ok) {
|
|
71
|
-
return []
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const body = await response.json()
|
|
75
|
-
|
|
76
|
-
if (!body || !Array.isArray(body.plugins)) {
|
|
77
|
-
return []
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return body.plugins as Skill[]
|
|
81
|
-
} catch {
|
|
82
|
-
return []
|
|
83
|
-
} finally {
|
|
84
|
-
clearTimeout(timeoutId)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Registers a new skill on the LiteLLM Skills Gateway.
|
|
90
|
-
* The skill points to a git source containing a SKILL.md file.
|
|
91
|
-
*/
|
|
92
|
-
export async function registerSkill(
|
|
93
|
-
config: PluginConfig,
|
|
94
|
-
token: string,
|
|
95
|
-
name: string,
|
|
96
|
-
gitUrl: string,
|
|
97
|
-
gitPath: string,
|
|
98
|
-
description?: string,
|
|
99
|
-
domain?: string,
|
|
100
|
-
): Promise<string> {
|
|
101
|
-
const controller = new AbortController()
|
|
102
|
-
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const response = await fetch(`${config.url}/claude-code/plugins`, {
|
|
106
|
-
method: 'POST',
|
|
107
|
-
headers: {
|
|
108
|
-
Authorization: `Bearer ${token}`,
|
|
109
|
-
'Content-Type': 'application/json',
|
|
110
|
-
},
|
|
111
|
-
body: JSON.stringify({
|
|
112
|
-
name,
|
|
113
|
-
source: {
|
|
114
|
-
source: 'git-subdir',
|
|
115
|
-
url: gitUrl,
|
|
116
|
-
path: gitPath,
|
|
117
|
-
},
|
|
118
|
-
description: description || null,
|
|
119
|
-
domain: domain || null,
|
|
120
|
-
}),
|
|
121
|
-
signal: controller.signal,
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
if (!response.ok) {
|
|
125
|
-
return `Error registering skill: HTTP ${response.status}`
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const body = await response.json()
|
|
129
|
-
const id = body?.plugin?.id ?? 'unknown'
|
|
130
|
-
return `Skill "${name}" registered (id: ${id})`
|
|
131
|
-
} catch (error: unknown) {
|
|
132
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
133
|
-
return `Error registering skill: ${message}`
|
|
134
|
-
} finally {
|
|
135
|
-
clearTimeout(timeoutId)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Enables (publishes) a skill on the LiteLLM Skills Gateway.
|
|
141
|
-
*/
|
|
142
|
-
export async function enableSkill(
|
|
143
|
-
config: PluginConfig,
|
|
144
|
-
token: string,
|
|
145
|
-
name: string,
|
|
146
|
-
): Promise<string> {
|
|
147
|
-
const controller = new AbortController()
|
|
148
|
-
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const response = await fetch(`${config.url}/claude-code/plugins/${name}/enable`, {
|
|
152
|
-
method: 'POST',
|
|
153
|
-
headers: {
|
|
154
|
-
Authorization: `Bearer ${token}`,
|
|
155
|
-
},
|
|
156
|
-
signal: controller.signal,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
return `Error enabling skill: HTTP ${response.status}`
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return `Skill "${name}" enabled`
|
|
164
|
-
} catch (error: unknown) {
|
|
165
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
166
|
-
return `Error enabling skill: ${message}`
|
|
167
|
-
} finally {
|
|
168
|
-
clearTimeout(timeoutId)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Disables (unpublishes) a skill on the LiteLLM Skills Gateway.
|
|
174
|
-
*/
|
|
175
|
-
export async function disableSkill(
|
|
176
|
-
config: PluginConfig,
|
|
177
|
-
token: string,
|
|
178
|
-
name: string,
|
|
179
|
-
): Promise<string> {
|
|
180
|
-
const controller = new AbortController()
|
|
181
|
-
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const response = await fetch(`${config.url}/claude-code/plugins/${name}/disable`, {
|
|
185
|
-
method: 'POST',
|
|
186
|
-
headers: {
|
|
187
|
-
Authorization: `Bearer ${token}`,
|
|
188
|
-
},
|
|
189
|
-
signal: controller.signal,
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
if (!response.ok) {
|
|
193
|
-
return `Error disabling skill: HTTP ${response.status}`
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return `Skill "${name}" disabled`
|
|
197
|
-
} catch (error: unknown) {
|
|
198
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
199
|
-
return `Error disabling skill: ${message}`
|
|
200
|
-
} finally {
|
|
201
|
-
clearTimeout(timeoutId)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Fetches the SKILL.md content from a skill's git source.
|
|
207
|
-
* Currently supports GitHub raw URLs.
|
|
208
|
-
*/
|
|
209
|
-
export async function fetchSkillContent(skill: Skill): Promise<string | null> {
|
|
210
|
-
const controller = new AbortController()
|
|
211
|
-
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
const rawUrl = buildRawGitUrl(skill.source)
|
|
215
|
-
if (!rawUrl) return null
|
|
216
|
-
|
|
217
|
-
const response = await fetch(rawUrl, {
|
|
218
|
-
signal: controller.signal,
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
if (!response.ok) return null
|
|
222
|
-
|
|
223
|
-
return await response.text()
|
|
224
|
-
} catch {
|
|
225
|
-
return null
|
|
226
|
-
} finally {
|
|
227
|
-
clearTimeout(timeoutId)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Builds a raw git URL for the SKILL.md file from a skill's source.
|
|
233
|
-
* Currently supports GitHub git-subdir sources.
|
|
234
|
-
*/
|
|
235
|
-
function buildRawGitUrl(source: Skill['source']): string | null {
|
|
236
|
-
if (source.source !== 'git-subdir') return null
|
|
237
|
-
|
|
238
|
-
const url = source.url
|
|
239
|
-
if (!url.includes('github.com')) return null
|
|
240
|
-
|
|
241
|
-
const isRaw = url.startsWith('https://raw.githubusercontent.com')
|
|
242
|
-
if (isRaw) {
|
|
243
|
-
const branch = extractBranch(url)
|
|
244
|
-
const path = source.path || ''
|
|
245
|
-
return `https://raw.githubusercontent.com/${url.replace('https://raw.githubusercontent.com/', '').split('/').slice(0, 2).join('/')}/${branch}/${path}/SKILL.md`
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const match = url.match(/https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?/)
|
|
249
|
-
if (!match) return null
|
|
250
|
-
|
|
251
|
-
const [, owner, repo] = match
|
|
252
|
-
const branch = extractBranch(url) || 'master'
|
|
253
|
-
const path = source.path || ''
|
|
254
|
-
|
|
255
|
-
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}/SKILL.md`
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Extracts the branch name from a GitHub URL.
|
|
260
|
-
* Falls back to 'master' if not found.
|
|
261
|
-
*/
|
|
262
|
-
function extractBranch(url: string): string | null {
|
|
263
|
-
const match = url.match(/\/tree\/([^/]+)/)
|
|
264
|
-
return match ? match[1] : null
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Creates opencode tool definitions for skill management operations.
|
|
269
|
-
* Returns tools: skill_list, skill_register, skill_enable, skill_disable.
|
|
270
|
-
*/
|
|
271
|
-
export function createSkillToolDefinitions(
|
|
272
|
-
config: PluginConfig,
|
|
273
|
-
token: string,
|
|
274
|
-
): Record<string, any> {
|
|
275
|
-
return {
|
|
276
|
-
skill_list: tool({
|
|
277
|
-
description: 'List all skills registered on the LiteLLM Skills Gateway',
|
|
278
|
-
args: {},
|
|
279
|
-
async execute(_args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
280
|
-
const skills = await listSkills(config, token)
|
|
281
|
-
|
|
282
|
-
if (skills.length === 0) {
|
|
283
|
-
return 'No skills found.'
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const header = '| Name | Description | Enabled | Source |'
|
|
287
|
-
const sep = '|--------|-------------|---------|--------|'
|
|
288
|
-
const rows = skills
|
|
289
|
-
.map(
|
|
290
|
-
(s) =>
|
|
291
|
-
`| ${s.name} | ${s.description || '-'} | ${s.enabled ? 'yes' : 'no'} | ${s.source.url} |`,
|
|
292
|
-
)
|
|
293
|
-
.join('\n')
|
|
294
|
-
|
|
295
|
-
return [header, sep, ...rows.split('\n')].join('\n')
|
|
296
|
-
},
|
|
297
|
-
}),
|
|
298
|
-
|
|
299
|
-
skill_register: tool({
|
|
300
|
-
description: 'Register a new skill on the LiteLLM Skills Gateway pointing to a git source',
|
|
301
|
-
args: {
|
|
302
|
-
name: tool.schema.string().describe('Name of the skill'),
|
|
303
|
-
git_url: tool.schema.string().describe('GitHub repository URL containing the skill'),
|
|
304
|
-
git_path: tool.schema.string().describe('Path within the repo to the skill directory (must contain SKILL.md)'),
|
|
305
|
-
description: tool.schema.string().optional().describe('Description of the skill'),
|
|
306
|
-
domain: tool.schema.string().optional().describe('Domain/category for the skill'),
|
|
307
|
-
},
|
|
308
|
-
async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
309
|
-
return registerSkill(
|
|
310
|
-
config,
|
|
311
|
-
token,
|
|
312
|
-
args.name as string,
|
|
313
|
-
args.git_url as string,
|
|
314
|
-
args.git_path as string,
|
|
315
|
-
args.description as string | undefined,
|
|
316
|
-
args.domain as string | undefined,
|
|
317
|
-
)
|
|
318
|
-
},
|
|
319
|
-
}),
|
|
320
|
-
|
|
321
|
-
skill_enable: tool({
|
|
322
|
-
description: 'Enable (publish) a skill on the LiteLLM Skills Gateway',
|
|
323
|
-
args: {
|
|
324
|
-
name: tool.schema.string().describe('Name of the skill to enable'),
|
|
325
|
-
},
|
|
326
|
-
async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
327
|
-
return enableSkill(config, token, args.name as string)
|
|
328
|
-
},
|
|
329
|
-
}),
|
|
330
|
-
|
|
331
|
-
skill_disable: tool({
|
|
332
|
-
description: 'Disable (unpublish) a skill on the LiteLLM Skills Gateway',
|
|
333
|
-
args: {
|
|
334
|
-
name: tool.schema.string().describe('Name of the skill to disable'),
|
|
335
|
-
},
|
|
336
|
-
async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
337
|
-
return disableSkill(config, token, args.name as string)
|
|
338
|
-
},
|
|
339
|
-
}),
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Creates a chat.message hook that injects active skills as context.
|
|
345
|
-
* Uses in-memory cache with 60s TTL to avoid hammering the API.
|
|
346
|
-
* Only injects for main agent sessions — skips all sub-agents.
|
|
347
|
-
*/
|
|
348
|
-
export function createSkillsInjector(
|
|
349
|
-
config: PluginConfig,
|
|
350
|
-
token: string,
|
|
351
|
-
): (
|
|
352
|
-
input: { sessionID: string; agent?: string; model?: any; messageID?: string; variant?: string },
|
|
353
|
-
output: { message: any; parts: any[] },
|
|
354
|
-
) => Promise<void> {
|
|
355
|
-
return async (input, output) => {
|
|
356
|
-
if (input.agent) return
|
|
357
|
-
|
|
358
|
-
let skills: Skill[] = []
|
|
359
|
-
if (skillsCache && Date.now() - skillsCache.timestamp < CACHE_TTL_MS) {
|
|
360
|
-
skills = skillsCache.data
|
|
361
|
-
} else {
|
|
362
|
-
skills = await listSkills(config, token)
|
|
363
|
-
skillsCache = { data: skills, timestamp: Date.now() }
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const enabledSkills = skills.filter((s) => s.enabled !== false)
|
|
367
|
-
if (enabledSkills.length === 0) return
|
|
368
|
-
|
|
369
|
-
const context = enabledSkills
|
|
370
|
-
.map((s) => `<skill name="${s.name}">${s.description || 'No description'}</skill>`)
|
|
371
|
-
.join('\n')
|
|
372
|
-
|
|
373
|
-
output.parts.push({ type: 'text', text: context })
|
|
374
|
-
}
|
|
375
|
-
}
|