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/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
- }