opencode-provider-litellm 0.5.1 → 0.6.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 +29 -20
- package/package.json +1 -1
- package/src/discovery.test.ts +47 -2
- package/src/discovery.ts +5 -4
- package/src/model-cache.test.ts +115 -0
- package/src/model-cache.ts +49 -0
- package/src/plugin.test.ts +6 -0
- package/src/plugin.ts +18 -4
package/README.md
CHANGED
|
@@ -21,34 +21,39 @@ All models and MCP tools from your LiteLLM proxy appear in OpenCode automaticall
|
|
|
21
21
|
|
|
22
22
|
### Environment variables
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|----------|-------------|
|
|
26
|
-
| `LITELLM_URL` | Your LiteLLM proxy base URL |
|
|
27
|
-
| `LITELLM_KEY` | API key for the proxy |
|
|
28
|
-
| `LITELLM_PROVIDER_ID` | Provider ID in OpenCode (defaults to `LiteLLM`) |
|
|
29
|
-
| `LITELLM_GCLOUD_TOKEN_AUTH` | Set to `1` to use Google ADC for auth (makes `LITELLM_KEY` optional) |
|
|
30
|
-
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to a Google ADC JSON file (used when `LITELLM_GCLOUD_TOKEN_AUTH=1`) |
|
|
24
|
+
Environment variables take precedence over all other configuration. Use them to keep secrets out of checked-in files.
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
| Variable | Required | Description |
|
|
27
|
+
|----------|----------|-------------|
|
|
28
|
+
| `LITELLM_URL` | Yes | Your LiteLLM proxy base URL |
|
|
29
|
+
| `LITELLM_KEY` | Yes* | API key for the proxy |
|
|
30
|
+
| `LITELLM_PROVIDER_ID` | No | Provider name in OpenCode (default: `LiteLLM`) |
|
|
31
|
+
| `LITELLM_GCLOUD_TOKEN_AUTH` | No | Set to `1` to use Google ADC for auth — makes `LITELLM_KEY` optional |
|
|
32
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | No | Path to a Google ADC JSON file (used when `LITELLM_GCLOUD_TOKEN_AUTH=1`) |
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
*`LITELLM_KEY` is optional when `LITELLM_GCLOUD_TOKEN_AUTH=1`.
|
|
35
|
+
|
|
36
|
+
### Plugin options
|
|
37
|
+
|
|
38
|
+
All env vars have an equivalent in the plugin options block in `opencode.json`. **Env vars take precedence** — use options for defaults that can be overridden per environment.
|
|
35
39
|
|
|
36
40
|
```jsonc
|
|
37
41
|
{
|
|
38
42
|
"plugin": [
|
|
39
43
|
["opencode-provider-litellm", {
|
|
40
|
-
"url": "https://your-litellm-proxy.example.com",
|
|
41
|
-
"apiKey": "sk-..."
|
|
44
|
+
"url": "https://your-litellm-proxy.example.com", // LITELLM_URL
|
|
45
|
+
"apiKey": "sk-...", // LITELLM_KEY
|
|
46
|
+
"providerName": "MyLiteLLM", // LITELLM_PROVIDER_ID
|
|
47
|
+
"gcloudTokenAuth": true // LITELLM_GCLOUD_TOKEN_AUTH=1
|
|
48
|
+
// apiKey can be omitted when gcloudTokenAuth is true
|
|
42
49
|
}]
|
|
43
50
|
]
|
|
44
51
|
}
|
|
45
52
|
```
|
|
46
53
|
|
|
47
|
-
> **Tip:** Environment variables take precedence over inline config. Use env vars to keep secrets out of checked-in files.
|
|
48
|
-
|
|
49
54
|
### Google Vertex AI (gcloud token auth)
|
|
50
55
|
|
|
51
|
-
When your LiteLLM proxy is backed by Google Vertex AI, you can skip `LITELLM_KEY` and let the plugin automatically fetch a
|
|
56
|
+
When your LiteLLM proxy is backed by Google Vertex AI, you can skip `LITELLM_KEY` and let the plugin automatically fetch a Google OAuth token:
|
|
52
57
|
|
|
53
58
|
```bash
|
|
54
59
|
# 1. Authenticate with gcloud (creates an ADC JSON file)
|
|
@@ -62,9 +67,12 @@ export LITELLM_GCLOUD_TOKEN_AUTH=1
|
|
|
62
67
|
opencode plugin opencode-provider-litellm
|
|
63
68
|
```
|
|
64
69
|
|
|
65
|
-
The plugin reads your [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) JSON file and exchanges the refresh token for an access token
|
|
70
|
+
The plugin reads your [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) JSON file and exchanges the refresh token for an access token. Tokens are cached for 50 minutes and used for both model discovery at startup and every LLM request.
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
**ADC file locations searched (in order):**
|
|
73
|
+
1. `GOOGLE_APPLICATION_CREDENTIALS` env var (all platforms)
|
|
74
|
+
2. `~/.config/gcloud/application_default_credentials.json` (Linux/macOS)
|
|
75
|
+
3. `%APPDATA%/gcloud/application_default_credentials.json` (Windows)
|
|
68
76
|
|
|
69
77
|
> **Note:** Only `authorized_user` credentials (from `gcloud auth application-default login`) are supported. Service account keys are not yet supported.
|
|
70
78
|
|
|
@@ -109,22 +117,23 @@ Skills appear in OpenCode's `/skills` menu and are loaded natively by the agent.
|
|
|
109
117
|
|
|
110
118
|
## How it works
|
|
111
119
|
|
|
112
|
-
The plugin uses
|
|
120
|
+
The plugin uses these OpenCode hooks:
|
|
113
121
|
|
|
114
122
|
| Hook | Purpose |
|
|
115
123
|
|------|---------|
|
|
116
124
|
| `config` | Discovers models from LiteLLM and injects them into OpenCode |
|
|
117
125
|
| `auth` | Provides a `/connect` entry point for pasting an API key |
|
|
118
126
|
| `tool` | Exposes discovered MCP tools as native OpenCode tools |
|
|
119
|
-
| `chat.headers` | Injects `Authorization: Bearer <token>` when
|
|
127
|
+
| `chat.headers` | Injects `Authorization: Bearer <token>` when gcloud token auth is enabled |
|
|
120
128
|
|
|
121
129
|
## Troubleshooting
|
|
122
130
|
|
|
123
131
|
| Problem | Solution |
|
|
124
132
|
|---------|----------|
|
|
125
|
-
| "Plugin config error" | Set `LITELLM_URL` and `LITELLM_KEY
|
|
133
|
+
| "Plugin config error" | Set `LITELLM_URL` and `LITELLM_KEY` (or `LITELLM_GCLOUD_TOKEN_AUTH=1`) |
|
|
126
134
|
| "Access denied" (403) | Verify the API key has access to the LiteLLM proxy |
|
|
127
|
-
| "No models discovered" | Check
|
|
135
|
+
| "No models discovered" | Check the proxy is reachable and `/health` responds |
|
|
136
|
+
| No models with gcloud auth | Verify `gcloud auth application-default login` has been run and the ADC file exists |
|
|
128
137
|
| Skills not showing | Verify the proxy-sidecar is running and the skills URL is in `opencode.json` |
|
|
129
138
|
|
|
130
139
|
## Development
|
package/package.json
CHANGED
package/src/discovery.test.ts
CHANGED
|
@@ -83,7 +83,7 @@ describe('discoverModels', () => {
|
|
|
83
83
|
tool_call: true,
|
|
84
84
|
reasoning: false,
|
|
85
85
|
limit: { context: 8192, output: 8192 },
|
|
86
|
-
cost: { input:
|
|
86
|
+
cost: { input: 100, output: 300 },
|
|
87
87
|
modalities: { input: ['text'], output: ['text'] },
|
|
88
88
|
},
|
|
89
89
|
'qwen3-32b': {
|
|
@@ -91,7 +91,7 @@ describe('discoverModels', () => {
|
|
|
91
91
|
tool_call: true,
|
|
92
92
|
reasoning: true,
|
|
93
93
|
limit: { context: 32768, output: 32768 },
|
|
94
|
-
cost: { input:
|
|
94
|
+
cost: { input: 50, output: 150 },
|
|
95
95
|
modalities: { input: ['text'], output: ['text'] },
|
|
96
96
|
},
|
|
97
97
|
})
|
|
@@ -107,6 +107,51 @@ describe('discoverModels', () => {
|
|
|
107
107
|
expect(getToken).toHaveBeenCalled()
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
+
it('converts per-token cost to per-1M tokens with cache costs', async () => {
|
|
111
|
+
const mockFetch = vi.fn()
|
|
112
|
+
.mockResolvedValueOnce({
|
|
113
|
+
ok: true,
|
|
114
|
+
status: 200,
|
|
115
|
+
json: async () => ({
|
|
116
|
+
healthy_endpoints: [
|
|
117
|
+
{ model: 'anthropic/claude-sonnet', model_id: 'uuid-1' },
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
.mockResolvedValueOnce({
|
|
122
|
+
ok: true,
|
|
123
|
+
status: 200,
|
|
124
|
+
json: async () => ({
|
|
125
|
+
data: [{
|
|
126
|
+
model_name: 'anthropic/claude-sonnet',
|
|
127
|
+
model_info: {
|
|
128
|
+
max_input_tokens: 1_000_000,
|
|
129
|
+
max_output_tokens: 64_000,
|
|
130
|
+
supports_function_calling: true,
|
|
131
|
+
supports_reasoning: true,
|
|
132
|
+
supports_vision: true,
|
|
133
|
+
supports_pdf_input: true,
|
|
134
|
+
// Per-token costs (LiteLLM format)
|
|
135
|
+
input_cost_per_token: 0.000005,
|
|
136
|
+
output_cost_per_token: 0.000025,
|
|
137
|
+
cache_read_input_token_cost: 0.0000005,
|
|
138
|
+
cache_creation_input_token_cost: 0.00000375,
|
|
139
|
+
},
|
|
140
|
+
}],
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
144
|
+
|
|
145
|
+
const result = await discoverModels(config, getToken)
|
|
146
|
+
|
|
147
|
+
expect(result['anthropic/claude-sonnet']?.cost).toEqual({
|
|
148
|
+
input: 5, // 0.000005 * 1M
|
|
149
|
+
output: 25, // 0.000025 * 1M
|
|
150
|
+
cache_read: 0.5, // 0.0000005 * 1M
|
|
151
|
+
cache_write: 3.75, // 0.00000375 * 1M
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
110
155
|
it('returns empty object on timeout', async () => {
|
|
111
156
|
vi.useFakeTimers()
|
|
112
157
|
|
package/src/discovery.ts
CHANGED
|
@@ -117,16 +117,17 @@ export async function discoverModels(
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// Add cost info if available
|
|
120
|
+
// LiteLLM returns cost per single token; opencode expects cost per 1M tokens
|
|
120
121
|
if (info.input_cost_per_token != null && info.output_cost_per_token != null) {
|
|
121
122
|
modelConfig.cost = {
|
|
122
|
-
input: info.input_cost_per_token,
|
|
123
|
-
output: info.output_cost_per_token,
|
|
123
|
+
input: info.input_cost_per_token * 1_000_000,
|
|
124
|
+
output: info.output_cost_per_token * 1_000_000,
|
|
124
125
|
}
|
|
125
126
|
if (info.cache_read_input_token_cost != null) {
|
|
126
|
-
modelConfig.cost.cache_read = info.cache_read_input_token_cost
|
|
127
|
+
modelConfig.cost.cache_read = info.cache_read_input_token_cost * 1_000_000
|
|
127
128
|
}
|
|
128
129
|
if (info.cache_creation_input_token_cost != null) {
|
|
129
|
-
modelConfig.cost.cache_write = info.cache_creation_input_token_cost
|
|
130
|
+
modelConfig.cost.cache_write = info.cache_creation_input_token_cost * 1_000_000
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import type { OpencodeModelConfig } from './types.js'
|
|
3
|
+
|
|
4
|
+
const mockReadFileSync = vi.hoisted(() => vi.fn())
|
|
5
|
+
const mockWriteFileSync = vi.hoisted(() => vi.fn())
|
|
6
|
+
|
|
7
|
+
vi.mock('fs', () => ({
|
|
8
|
+
existsSync: vi.fn(() => true),
|
|
9
|
+
readFileSync: mockReadFileSync,
|
|
10
|
+
writeFileSync: mockWriteFileSync,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('os', () => ({
|
|
14
|
+
homedir: () => '/home/test',
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
const { loadModelCache, saveModelCache } = await import('./model-cache.js')
|
|
18
|
+
|
|
19
|
+
const sampleModels: Record<string, OpencodeModelConfig> = {
|
|
20
|
+
'anthropic/claude-sonnet': {
|
|
21
|
+
name: 'anthropic/claude-sonnet',
|
|
22
|
+
tool_call: true,
|
|
23
|
+
reasoning: true,
|
|
24
|
+
limit: { context: 1_000_000, output: 64_000 },
|
|
25
|
+
cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
26
|
+
modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
|
|
27
|
+
},
|
|
28
|
+
'qwen/qwen3.6-27b': {
|
|
29
|
+
name: 'qwen/qwen3.6-27b',
|
|
30
|
+
tool_call: true,
|
|
31
|
+
reasoning: false,
|
|
32
|
+
limit: { context: 262144, output: 32768 },
|
|
33
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('loadModelCache', () => {
|
|
38
|
+
afterEach(() => vi.clearAllMocks())
|
|
39
|
+
|
|
40
|
+
it('returns null when file does not exist', () => {
|
|
41
|
+
mockReadFileSync.mockImplementation(() => { throw new Error('ENOENT') })
|
|
42
|
+
expect(loadModelCache('protector')).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns null when file contains invalid JSON', () => {
|
|
46
|
+
mockReadFileSync.mockReturnValue('not valid json{{{')
|
|
47
|
+
expect(loadModelCache('protector')).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns null when providerId does not match', () => {
|
|
51
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
52
|
+
savedAt: Date.now(), providerId: 'other', models: sampleModels,
|
|
53
|
+
}))
|
|
54
|
+
expect(loadModelCache('protector')).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns null when models field is missing', () => {
|
|
58
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
59
|
+
savedAt: Date.now(), providerId: 'protector', models: null,
|
|
60
|
+
}))
|
|
61
|
+
expect(loadModelCache('protector')).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns models when cache is valid', () => {
|
|
65
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
66
|
+
savedAt: Date.now(), providerId: 'protector', models: sampleModels,
|
|
67
|
+
}))
|
|
68
|
+
expect(loadModelCache('protector')).toEqual(sampleModels)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('reads from the correct path', () => {
|
|
72
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({
|
|
73
|
+
savedAt: Date.now(), providerId: 'protector', models: sampleModels,
|
|
74
|
+
}))
|
|
75
|
+
loadModelCache('protector')
|
|
76
|
+
expect(mockReadFileSync).toHaveBeenCalledWith(
|
|
77
|
+
expect.stringContaining('opencode-provider-litellm-cache.json'),
|
|
78
|
+
'utf-8',
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('saveModelCache', () => {
|
|
84
|
+
afterEach(() => vi.clearAllMocks())
|
|
85
|
+
|
|
86
|
+
it('writes a valid cache file', () => {
|
|
87
|
+
saveModelCache('protector', sampleModels)
|
|
88
|
+
expect(mockWriteFileSync).toHaveBeenCalledOnce()
|
|
89
|
+
const [filePath, content] = mockWriteFileSync.mock.calls[0] as [string, string, string]
|
|
90
|
+
expect(filePath).toContain('opencode-provider-litellm-cache.json')
|
|
91
|
+
const parsed = JSON.parse(content)
|
|
92
|
+
expect(parsed.providerId).toBe('protector')
|
|
93
|
+
expect(parsed.models).toEqual(sampleModels)
|
|
94
|
+
expect(typeof parsed.savedAt).toBe('number')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('writes to the correct path under ~/.local/share/opencode/', () => {
|
|
98
|
+
saveModelCache('protector', sampleModels)
|
|
99
|
+
const [filePath] = mockWriteFileSync.mock.calls[0] as [string, string, string]
|
|
100
|
+
expect(filePath).toMatch(/\.local[/\\]share[/\\]opencode[/\\]opencode-provider-litellm-cache\.json/)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('does not throw when writeFileSync fails', () => {
|
|
104
|
+
mockWriteFileSync.mockImplementation(() => { throw new Error('EACCES') })
|
|
105
|
+
expect(() => saveModelCache('protector', sampleModels)).not.toThrow()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('round-trips correctly with loadModelCache', () => {
|
|
109
|
+
let written = ''
|
|
110
|
+
mockWriteFileSync.mockImplementation((_p: string, content: string) => { written = content })
|
|
111
|
+
mockReadFileSync.mockImplementation(() => written)
|
|
112
|
+
saveModelCache('protector', sampleModels)
|
|
113
|
+
expect(loadModelCache('protector')).toEqual(sampleModels)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
import type { OpencodeModelConfig } from './types.js'
|
|
5
|
+
|
|
6
|
+
const CACHE_FILENAME = 'opencode-provider-litellm-cache.json'
|
|
7
|
+
|
|
8
|
+
interface ModelCache {
|
|
9
|
+
savedAt: number
|
|
10
|
+
providerId: string
|
|
11
|
+
models: Record<string, OpencodeModelConfig>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getCachePath(): string {
|
|
15
|
+
return join(homedir(), '.local', 'share', 'opencode', CACHE_FILENAME)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads the model cache from disk. Returns null if the file does not exist,
|
|
20
|
+
* cannot be parsed, or belongs to a different provider.
|
|
21
|
+
*/
|
|
22
|
+
export function loadModelCache(providerId: string): Record<string, OpencodeModelConfig> | null {
|
|
23
|
+
try {
|
|
24
|
+
const raw = readFileSync(getCachePath(), 'utf-8')
|
|
25
|
+
const cache = JSON.parse(raw) as ModelCache
|
|
26
|
+
if (cache.providerId !== providerId) return null
|
|
27
|
+
if (!cache.models || typeof cache.models !== 'object') return null
|
|
28
|
+
return cache.models
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Saves the discovered models to the cache file on disk. Failures are
|
|
36
|
+
* non-fatal — discovery already succeeded.
|
|
37
|
+
*/
|
|
38
|
+
export function saveModelCache(providerId: string, models: Record<string, OpencodeModelConfig>): void {
|
|
39
|
+
try {
|
|
40
|
+
const cache: ModelCache = {
|
|
41
|
+
savedAt: Date.now(),
|
|
42
|
+
providerId,
|
|
43
|
+
models,
|
|
44
|
+
}
|
|
45
|
+
writeFileSync(getCachePath(), JSON.stringify(cache, null, 2), 'utf-8')
|
|
46
|
+
} catch {
|
|
47
|
+
// Non-fatal — cache will be written next time
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/plugin.test.ts
CHANGED
|
@@ -26,6 +26,12 @@ vi.mock('./gcloud-token.js', () => ({
|
|
|
26
26
|
resetTokenCache: vi.fn(),
|
|
27
27
|
}))
|
|
28
28
|
|
|
29
|
+
// Mock the model cache module — no cache by default
|
|
30
|
+
vi.mock('./model-cache.js', () => ({
|
|
31
|
+
loadModelCache: vi.fn().mockReturnValue(null),
|
|
32
|
+
saveModelCache: vi.fn(),
|
|
33
|
+
}))
|
|
34
|
+
|
|
29
35
|
import { LiteLLMPlugin } from './plugin.js'
|
|
30
36
|
import { discoverModels, injectModelsIntoConfig } from './discovery.js'
|
|
31
37
|
import { resolvePluginConfig } from './utils.js'
|
package/src/plugin.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { resolvePluginConfig, getProviderId } from './utils.js'
|
|
|
3
3
|
import { discoverModels, injectModelsIntoConfig } from './discovery.js'
|
|
4
4
|
import { createMcpToolDefinitions } from './mcp-tools.js'
|
|
5
5
|
import { getGcloudToken } from './gcloud-token.js'
|
|
6
|
+
import { loadModelCache, saveModelCache } from './model-cache.js'
|
|
6
7
|
|
|
7
8
|
export const LiteLLMPlugin: Plugin = async (
|
|
8
9
|
input: PluginInput,
|
|
@@ -45,11 +46,23 @@ export const LiteLLMPlugin: Plugin = async (
|
|
|
45
46
|
|
|
46
47
|
const result: Record<string, unknown> = {
|
|
47
48
|
config: async (config: Record<string, any>) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Inject cached models immediately so opencode has something to work
|
|
50
|
+
// with while live discovery runs.
|
|
51
|
+
const cachedModels = loadModelCache(providerId)
|
|
52
|
+
if (cachedModels) {
|
|
53
|
+
const token = await getToken()
|
|
54
|
+
injectModelsIntoConfig(
|
|
55
|
+
config as Parameters<typeof injectModelsIntoConfig>[0],
|
|
56
|
+
providerId,
|
|
57
|
+
pluginConfig.url,
|
|
58
|
+
token,
|
|
59
|
+
cachedModels,
|
|
52
60
|
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Discover live models, update cache, and re-inject with fresh data.
|
|
64
|
+
try {
|
|
65
|
+
const models = await discoverModels(pluginConfig, getToken)
|
|
53
66
|
|
|
54
67
|
if (Object.keys(models).length === 0) {
|
|
55
68
|
await input.client.app.log({
|
|
@@ -60,6 +73,7 @@ export const LiteLLMPlugin: Plugin = async (
|
|
|
60
73
|
},
|
|
61
74
|
})
|
|
62
75
|
} else {
|
|
76
|
+
saveModelCache(providerId, models)
|
|
63
77
|
const token = await getToken()
|
|
64
78
|
injectModelsIntoConfig(
|
|
65
79
|
config as Parameters<typeof injectModelsIntoConfig>[0],
|