opencode-models-discovery 0.6.0 → 0.7.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 +78 -14
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/monitoring/loading-monitor.ts +6 -6
- package/src/plugin/config-hook.ts +1 -5
- package/src/plugin/enhance-config.ts +17 -35
- package/src/plugin/index.ts +4 -4
- package/src/types/index.ts +0 -56
- package/src/types/plugin-config.ts +38 -5
- package/src/ui/toast-notifier.ts +14 -14
- package/src/utils/index.ts +0 -200
- package/src/cache/model-status-cache.ts +0 -122
- package/src/plugin/get-loaded-models.ts +0 -10
package/README.md
CHANGED
|
@@ -2,23 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/opencode-models-discovery)
|
|
4
4
|
[](https://www.npmjs.com/package/opencode-models-discovery)
|
|
5
|
+
[](https://github.com/yuhp/opencode-models-discovery/actions/workflows/release.yml)
|
|
6
|
+
[](https://github.com/yuhp/opencode-models-discovery/blob/main/LICENSE)
|
|
7
|
+
[](https://opencode.ai)
|
|
5
8
|
|
|
6
|
-
>
|
|
9
|
+
> A universal OpenCode plugin for dynamic model discovery across **any OpenAI-compatible provider**.
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
Originally inspired by [opencode-lmstudio](https://github.com/nicktasios/opencode-lmstudio), this project has been fully refactored into a general-purpose model discovery plugin with richer configuration controls for providers, models, naming, caching, and discovery behavior.
|
|
9
12
|
|
|
10
13
|
## Features
|
|
11
14
|
|
|
12
|
-
- **
|
|
13
|
-
- **Dynamic Model Discovery**: Queries provider's `/v1/models` endpoint to discover available models
|
|
14
|
-
- **Auto-Injection**: Automatically adds unconfigured models
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
15
|
+
- **Universal Provider Support**: Works with any OpenAI-compatible provider (LM Studio, Ollama, LocalAI, gateways, and more)
|
|
16
|
+
- **Dynamic Model Discovery**: Queries each provider's `/v1/models` endpoint to discover available models
|
|
17
|
+
- **Auto-Injection**: Automatically adds unconfigured models into OpenCode provider config
|
|
18
|
+
- **Provider Filtering**: Include or exclude specific providers from discovery
|
|
19
|
+
- **Model Filtering**: Use regex rules to precisely control which discovered models are injected
|
|
20
|
+
- **Configurable Discovery**: Control discovery behavior with enable/disable switches and TTL-based caching
|
|
21
|
+
- **Smart Model Formatting**: Optional human-friendly display names for discovered models
|
|
22
|
+
- **Organization Owner Extraction**: Extracts and sets `organizationOwner` from model IDs when available
|
|
23
|
+
- **Health Check Monitoring**: Verifies providers are accessible before attempting discovery
|
|
18
24
|
- **Model Merging**: Intelligently merges discovered models with existing configuration
|
|
19
|
-
- **
|
|
20
|
-
- **Error Handling**: Smart error categorization with auto-fix suggestions
|
|
21
|
-
- **Configurable Discovery**: Enable/disable discovery and filter providers
|
|
25
|
+
- **Error Handling**: Smart error categorization with actionable suggestions
|
|
22
26
|
|
|
23
27
|
## Installation
|
|
24
28
|
|
|
@@ -74,8 +78,7 @@ The plugin configuration is placed in the `plugin` array using tuple format `["p
|
|
|
74
78
|
"excludeRegex": []
|
|
75
79
|
},
|
|
76
80
|
"discovery": {
|
|
77
|
-
"enabled": true
|
|
78
|
-
"ttl": 15000
|
|
81
|
+
"enabled": true
|
|
79
82
|
},
|
|
80
83
|
"smartModelName": false
|
|
81
84
|
}]
|
|
@@ -85,6 +88,67 @@ The plugin configuration is placed in the `plugin` array using tuple format `["p
|
|
|
85
88
|
|
|
86
89
|
Set `smartModelName` to `true` if you want discovered models to use human-friendly display names instead of the raw `model_id`. (e.g., "Qwen3 30B A3B" instead of "qwen/qwen3-30b-a3b")
|
|
87
90
|
|
|
91
|
+
#### Provider-Level Discovery Overrides
|
|
92
|
+
|
|
93
|
+
Each provider can override discovery behavior through `provider.<name>.options.modelsDiscovery`:
|
|
94
|
+
|
|
95
|
+
| Option | Type | Description |
|
|
96
|
+
|--------|------|-------------|
|
|
97
|
+
| `provider.<name>.options.modelsDiscovery.enabled` | `boolean` | Override global discovery and provider filters for a single provider |
|
|
98
|
+
| `provider.<name>.options.modelsDiscovery.models.includeRegex` | `string[]` | Provider-specific model include filter |
|
|
99
|
+
| `provider.<name>.options.modelsDiscovery.models.excludeRegex` | `string[]` | Provider-specific model exclude filter |
|
|
100
|
+
| `provider.<name>.options.modelsDiscovery.smartModelName` | `boolean` | Override global `smartModelName` for a single provider |
|
|
101
|
+
|
|
102
|
+
Priority rules:
|
|
103
|
+
|
|
104
|
+
1. `provider.<name>.options.modelsDiscovery.enabled` overrides global `discovery.enabled` and `providers.include/exclude`
|
|
105
|
+
2. If a provider defines its own `modelsDiscovery.models` filters, those filters replace global `models.includeRegex/excludeRegex` for that provider
|
|
106
|
+
3. If a provider does not define its own model filters, global `models.includeRegex/excludeRegex` are used
|
|
107
|
+
4. `provider.<name>.options.modelsDiscovery.smartModelName` overrides global `smartModelName`
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"plugin": [
|
|
112
|
+
["opencode-models-discovery", {
|
|
113
|
+
"providers": {
|
|
114
|
+
"include": ["ollama"]
|
|
115
|
+
},
|
|
116
|
+
"models": {
|
|
117
|
+
"includeRegex": ["^qwen/"]
|
|
118
|
+
},
|
|
119
|
+
"discovery": {
|
|
120
|
+
"enabled": false
|
|
121
|
+
},
|
|
122
|
+
"smartModelName": false
|
|
123
|
+
}]
|
|
124
|
+
],
|
|
125
|
+
"provider": {
|
|
126
|
+
"lmstudio": {
|
|
127
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
128
|
+
"name": "LM Studio",
|
|
129
|
+
"options": {
|
|
130
|
+
"baseURL": "http://127.0.0.1:1234/v1",
|
|
131
|
+
"modelsDiscovery": {
|
|
132
|
+
"enabled": true,
|
|
133
|
+
"models": {
|
|
134
|
+
"includeRegex": ["^gpt-"]
|
|
135
|
+
},
|
|
136
|
+
"smartModelName": true
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"models": {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
In this example:
|
|
146
|
+
|
|
147
|
+
1. Global discovery is disabled
|
|
148
|
+
2. `lmstudio` is still discovered because `modelsDiscovery.enabled` is `true`
|
|
149
|
+
3. `lmstudio` uses `^gpt-` instead of the global `^qwen/` filter
|
|
150
|
+
4. `lmstudio` uses smart model names even though the global setting is `false`
|
|
151
|
+
|
|
88
152
|
#### Provider Filtering
|
|
89
153
|
|
|
90
154
|
Control which providers are discovered:
|
|
@@ -239,7 +303,7 @@ This means providers using `@ai-sdk/anthropic` with OpenAI-compatible backends (
|
|
|
239
303
|
|
|
240
304
|
## Logging
|
|
241
305
|
|
|
242
|
-
When available, the plugin writes logs through OpenCode's structured server log API via `client.app.log(...)` using the service name `opencode-
|
|
306
|
+
When available, the plugin writes logs through OpenCode's structured server log API via `client.app.log(...)` using the service name `opencode-models-discovery`.
|
|
243
307
|
|
|
244
308
|
If structured logging is unavailable in the runtime, the plugin falls back to prefixed `console.*` output. Key log categories are emitted through metadata such as `plugin`, `config`, `discovery`, `event`, and `filtering` to make local debugging easier with `opencode --print-logs`.
|
|
245
309
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-models-discovery",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "OpenCode plugin for auto-discovery of OpenAI-compatible models with dynamic provider configuration",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ModelDiscoveryPlugin
|
|
1
|
+
export { ModelDiscoveryPlugin } from './plugin'
|
|
@@ -21,7 +21,7 @@ export class ModelLoadingMonitor {
|
|
|
21
21
|
progress: 0
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
console.info(`[opencode-
|
|
24
|
+
console.info(`[opencode-models-discovery] Started monitoring model loading`, { modelId, baseURL })
|
|
25
25
|
|
|
26
26
|
// Clear any existing interval
|
|
27
27
|
this.stopMonitoring(modelId)
|
|
@@ -68,7 +68,7 @@ export class ModelLoadingMonitor {
|
|
|
68
68
|
const duration = Date.now() - (state.startTime || Date.now())
|
|
69
69
|
this.updateState(modelId, 'loaded', 100, 0)
|
|
70
70
|
this.stopMonitoring(modelId)
|
|
71
|
-
console.info(`[opencode-
|
|
71
|
+
console.info(`[opencode-models-discovery] Model loading completed`, {
|
|
72
72
|
modelId,
|
|
73
73
|
duration: `${duration}ms`,
|
|
74
74
|
totalModels: this.loadingStates.size
|
|
@@ -111,11 +111,11 @@ export class ModelLoadingMonitor {
|
|
|
111
111
|
// Log state changes
|
|
112
112
|
if (currentState.status !== status) {
|
|
113
113
|
if (status === 'loaded') {
|
|
114
|
-
console.info(`[opencode-
|
|
114
|
+
console.info(`[opencode-models-discovery] Model loading completed`, { modelId })
|
|
115
115
|
} else if (status === 'error') {
|
|
116
|
-
console.warn(`[opencode-
|
|
116
|
+
console.warn(`[opencode-models-discovery] Model loading failed`, { modelId, error })
|
|
117
117
|
} else if (status === 'loading') {
|
|
118
|
-
console.info(`[opencode-
|
|
118
|
+
console.info(`[opencode-models-discovery] Model loading started`, { modelId })
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
@@ -138,4 +138,4 @@ export class ModelLoadingMonitor {
|
|
|
138
138
|
}
|
|
139
139
|
this.loadingStates.clear()
|
|
140
140
|
}
|
|
141
|
-
}
|
|
141
|
+
}
|
|
@@ -20,7 +20,7 @@ export function createConfigHook(
|
|
|
20
20
|
const validation = validateConfig(config)
|
|
21
21
|
if (!validation.isValid) {
|
|
22
22
|
logger.error('Invalid config provided', { errors: validation.errors })
|
|
23
|
-
toastNotifier.error("Plugin configuration is invalid", "Configuration Error").catch(() => {})
|
|
23
|
+
toastNotifier.error("Plugin configuration is invalid", "Configuration Error").catch(() => { })
|
|
24
24
|
return
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -28,10 +28,6 @@ export function createConfigHook(
|
|
|
28
28
|
logger.warn('Config warnings', { warnings: validation.warnings })
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
if (pluginConfig.discovery?.enabled === false) {
|
|
32
|
-
logger.info('Discovery disabled by configuration')
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
31
|
|
|
36
32
|
const discoveryPromise = enhanceConfig(
|
|
37
33
|
config,
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import { ModelStatusCache } from '../cache/model-status-cache'
|
|
2
1
|
import { ToastNotifier } from '../ui/toast-notifier'
|
|
3
2
|
import { categorizeModel, formatModelName, extractModelOwner } from '../utils'
|
|
4
3
|
import { normalizeBaseURL, checkProviderHealth, discoverModelsFromProvider, autoDetectOpenAICompatibleProvider, canDiscoverModels } from '../utils/openai-compatible-api'
|
|
5
|
-
import { getProviderFilter, getDiscoveryConfig, getModelRegexFilter, shouldDiscoverModel,
|
|
4
|
+
import { getProviderFilter, getDiscoveryConfig, getModelRegexFilter, getProviderModelRegexFilter, shouldDiscoverModel, shouldDiscoverProviderWithOverride } from '../types/plugin-config'
|
|
6
5
|
import type { PluginLogger } from './logger'
|
|
7
6
|
import type { PluginInput } from '@opencode-ai/plugin'
|
|
8
7
|
import type { OpenAIModel } from '../types'
|
|
9
8
|
import type { PluginConfig } from '../types/plugin-config'
|
|
10
9
|
|
|
11
|
-
const modelStatusCache = new ModelStatusCache()
|
|
12
|
-
|
|
13
10
|
interface DiscoveredProvider {
|
|
14
11
|
name: string
|
|
15
12
|
baseURL: string
|
|
@@ -23,23 +20,24 @@ export async function enhanceConfig(
|
|
|
23
20
|
pluginConfig: PluginConfig,
|
|
24
21
|
logger: PluginLogger
|
|
25
22
|
): Promise<void> {
|
|
26
|
-
modelStatusCache.invalidateAll()
|
|
27
|
-
|
|
28
23
|
try {
|
|
29
24
|
const providers = config.provider || {}
|
|
30
25
|
const openAICompatibleProviders: DiscoveredProvider[] = []
|
|
31
26
|
const providerFilter = getProviderFilter(pluginConfig)
|
|
32
27
|
const modelRegexFilter = getModelRegexFilter(pluginConfig, logger.child({ category: 'filtering' }))
|
|
33
28
|
const discoveryConfig = getDiscoveryConfig(pluginConfig)
|
|
29
|
+
const globalDiscoveryEnabled = discoveryConfig.enabled
|
|
34
30
|
|
|
35
31
|
for (const [providerName, providerConfig] of Object.entries(providers)) {
|
|
36
32
|
const p = providerConfig as any
|
|
37
|
-
|
|
33
|
+
const providerDiscoveryConfig = p.options?.modelsDiscovery ?? {}
|
|
34
|
+
|
|
38
35
|
if (!canDiscoverModels(p)) {
|
|
39
36
|
continue
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
if (!
|
|
39
|
+
if (!shouldDiscoverProviderWithOverride(providerName, providerFilter, globalDiscoveryEnabled, providerDiscoveryConfig)) {
|
|
40
|
+
logger.debug(`Provider ${providerName} model discovery disabled by configuration`)
|
|
43
41
|
continue
|
|
44
42
|
}
|
|
45
43
|
|
|
@@ -81,11 +79,18 @@ export async function enhanceConfig(
|
|
|
81
79
|
let chatModelsCount = 0
|
|
82
80
|
let embeddingModelsCount = 0
|
|
83
81
|
|
|
82
|
+
const hasProviderModelRegexFilter = !!providerDiscoveryConfig.models?.includeRegex?.length || !!providerDiscoveryConfig.models?.excludeRegex?.length
|
|
83
|
+
const providerModelRegexFilter = getProviderModelRegexFilter(providerDiscoveryConfig, logger.child({ category: 'filtering' }))
|
|
84
|
+
let smartModelNameEnabled = providerDiscoveryConfig.smartModelName
|
|
85
|
+
if (smartModelNameEnabled === undefined) {
|
|
86
|
+
smartModelNameEnabled = pluginConfig.smartModelName
|
|
87
|
+
}
|
|
88
|
+
|
|
84
89
|
for (const model of models) {
|
|
85
90
|
const modelKey = model.id
|
|
86
|
-
|
|
87
91
|
if (!existingModels[modelKey]) {
|
|
88
|
-
|
|
92
|
+
const activeModelRegexFilter = hasProviderModelRegexFilter ? providerModelRegexFilter : modelRegexFilter
|
|
93
|
+
if (!shouldDiscoverModel(model.id, activeModelRegexFilter)) {
|
|
89
94
|
continue
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -93,7 +98,7 @@ export async function enhanceConfig(
|
|
|
93
98
|
const owner = extractModelOwner(model.id)
|
|
94
99
|
const modelConfig: any = {
|
|
95
100
|
id: model.id,
|
|
96
|
-
name:
|
|
101
|
+
name: smartModelNameEnabled ? formatModelName(model) : model.id,
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
if (owner) {
|
|
@@ -154,33 +159,10 @@ export async function enhanceConfig(
|
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
try {
|
|
158
|
-
for (const [providerName, providerConfig] of Object.entries(providers)) {
|
|
159
|
-
const p = providerConfig as any
|
|
160
|
-
if (!canDiscoverModels(p) || !shouldDiscoverProvider(providerName, providerFilter)) {
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
if (p.options?.baseURL) {
|
|
164
|
-
const baseURL = normalizeBaseURL(p.options.baseURL)
|
|
165
|
-
if (discoveryConfig.ttl) {
|
|
166
|
-
modelStatusCache.setTTL(baseURL, discoveryConfig.ttl)
|
|
167
|
-
}
|
|
168
|
-
if (!modelStatusCache.isValid(baseURL)) {
|
|
169
|
-
await modelStatusCache.getModels(baseURL, async () => {
|
|
170
|
-
return await discoverModelsFromProvider(baseURL).then(models => models.map(m => m.id))
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
} catch (error) {
|
|
176
|
-
logger.warn('Model status cache refresh failed', {
|
|
177
|
-
error: error instanceof Error ? error.message : String(error),
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
162
|
} catch (error) {
|
|
181
163
|
logger.error('Unexpected error in enhanceConfig', {
|
|
182
164
|
error: error instanceof Error ? error.message : String(error),
|
|
183
165
|
})
|
|
184
|
-
toastNotifier.warning("Plugin configuration failed", "Configuration Error").catch(() => {})
|
|
166
|
+
toastNotifier.warning("Plugin configuration failed", "Configuration Error").catch(() => { })
|
|
185
167
|
}
|
|
186
168
|
}
|
package/src/plugin/index.ts
CHANGED
|
@@ -13,9 +13,9 @@ export const ModelDiscoveryPlugin: Plugin = async (input: PluginInput, options?:
|
|
|
13
13
|
if (!client || typeof client !== 'object') {
|
|
14
14
|
logger.error('Invalid client provided to plugin')
|
|
15
15
|
return {
|
|
16
|
-
config: async () => {},
|
|
17
|
-
event: async () => {},
|
|
18
|
-
"chat.params": async () => {}
|
|
16
|
+
config: async () => { },
|
|
17
|
+
event: async () => { },
|
|
18
|
+
"chat.params": async () => { }
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -36,4 +36,4 @@ export const ModelDiscoveryPlugin: Plugin = async (input: PluginInput, options?:
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export const LMStudioPlugin = ModelDiscoveryPlugin
|
|
39
|
+
//export const LMStudioPlugin = ModelDiscoveryPlugin
|
package/src/types/index.ts
CHANGED
|
@@ -22,61 +22,5 @@ export interface ModelLoadingState {
|
|
|
22
22
|
error?: string
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export interface ModelValidationError {
|
|
26
|
-
type: 'offline' | 'not_found' | 'network' | 'permission' | 'timeout' | 'unknown'
|
|
27
|
-
severity: 'low' | 'medium' | 'high' | 'critical'
|
|
28
|
-
message: string
|
|
29
|
-
canRetry: boolean
|
|
30
|
-
autoFixAvailable: boolean
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface AutoFixSuggestion {
|
|
34
|
-
action: string
|
|
35
|
-
command?: string
|
|
36
|
-
steps?: string[]
|
|
37
|
-
automated: boolean
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface SimilarModel {
|
|
41
|
-
model: string
|
|
42
|
-
similarity: number
|
|
43
|
-
reason: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface CacheStats {
|
|
47
|
-
size: number
|
|
48
|
-
entries: Array<{
|
|
49
|
-
baseURL: string
|
|
50
|
-
age: number
|
|
51
|
-
modelCount: number
|
|
52
|
-
ttl: number
|
|
53
|
-
}>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface ModelValidationResult {
|
|
57
|
-
status: 'success' | 'error'
|
|
58
|
-
model: string
|
|
59
|
-
availableModels: string[]
|
|
60
|
-
message: string
|
|
61
|
-
errorCategory?: string
|
|
62
|
-
severity?: string
|
|
63
|
-
canRetry?: boolean
|
|
64
|
-
autoFixAvailable?: boolean
|
|
65
|
-
autoFixSuggestions?: AutoFixSuggestion[]
|
|
66
|
-
steps?: string[]
|
|
67
|
-
similarModels?: Array<{
|
|
68
|
-
model: string
|
|
69
|
-
similarity: number
|
|
70
|
-
reason: string
|
|
71
|
-
}>
|
|
72
|
-
cacheInfo?: {
|
|
73
|
-
age: number
|
|
74
|
-
valid: boolean
|
|
75
|
-
totalCacheEntries: number
|
|
76
|
-
}
|
|
77
|
-
performanceHint?: string
|
|
78
|
-
}
|
|
79
|
-
|
|
80
25
|
export type LMStudioModel = OpenAIModel
|
|
81
26
|
export type LMStudioModelsResponse = OpenAIModelsResponse
|
|
82
|
-
export type LMStudioValidationResult = ModelValidationResult
|
|
@@ -9,7 +9,15 @@ export interface PluginConfig {
|
|
|
9
9
|
}
|
|
10
10
|
discovery?: {
|
|
11
11
|
enabled?: boolean
|
|
12
|
-
|
|
12
|
+
}
|
|
13
|
+
smartModelName?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderDiscoveryConfig {
|
|
17
|
+
enabled?: boolean
|
|
18
|
+
models?: {
|
|
19
|
+
includeRegex?: string[]
|
|
20
|
+
excludeRegex?: string[]
|
|
13
21
|
}
|
|
14
22
|
smartModelName?: boolean
|
|
15
23
|
}
|
|
@@ -21,7 +29,6 @@ export interface ProviderFilter {
|
|
|
21
29
|
|
|
22
30
|
export interface DiscoveryConfig {
|
|
23
31
|
enabled: boolean
|
|
24
|
-
ttl: number
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface ModelRegexFilter {
|
|
@@ -31,7 +38,6 @@ export interface ModelRegexFilter {
|
|
|
31
38
|
|
|
32
39
|
export const DEFAULT_DISCOVERY_CONFIG: DiscoveryConfig = {
|
|
33
40
|
enabled: true,
|
|
34
|
-
ttl: 15000,
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function shouldDiscoverProvider(
|
|
@@ -54,10 +60,30 @@ export function getProviderFilter(config: PluginConfig): ProviderFilter {
|
|
|
54
60
|
export function getDiscoveryConfig(config: PluginConfig): DiscoveryConfig {
|
|
55
61
|
return {
|
|
56
62
|
enabled: config.discovery?.enabled ?? DEFAULT_DISCOVERY_CONFIG.enabled,
|
|
57
|
-
ttl: config.discovery?.ttl ?? DEFAULT_DISCOVERY_CONFIG.ttl,
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
export function shouldDiscoverProviderWithOverride(
|
|
67
|
+
providerName: string,
|
|
68
|
+
filter: ProviderFilter,
|
|
69
|
+
globalEnabled: boolean,
|
|
70
|
+
providerConfig: ProviderDiscoveryConfig
|
|
71
|
+
): boolean {
|
|
72
|
+
if (providerConfig.enabled === true) {
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (providerConfig.enabled === false) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!globalEnabled) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return shouldDiscoverProvider(providerName, filter)
|
|
85
|
+
}
|
|
86
|
+
|
|
61
87
|
function toRegExp(pattern: string, logger?: PluginLogger): RegExp | null {
|
|
62
88
|
try {
|
|
63
89
|
return new RegExp(pattern)
|
|
@@ -65,7 +91,7 @@ function toRegExp(pattern: string, logger?: PluginLogger): RegExp | null {
|
|
|
65
91
|
if (logger) {
|
|
66
92
|
logger.warn('Ignoring invalid model regex', { category: 'filtering', pattern })
|
|
67
93
|
} else {
|
|
68
|
-
console.warn(`[opencode-
|
|
94
|
+
console.warn(`[opencode-models-discovery] Ignoring invalid model regex: ${pattern}`)
|
|
69
95
|
}
|
|
70
96
|
return null
|
|
71
97
|
}
|
|
@@ -78,6 +104,13 @@ export function getModelRegexFilter(config: PluginConfig, logger?: PluginLogger)
|
|
|
78
104
|
}
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
export function getProviderModelRegexFilter(config: ProviderDiscoveryConfig, logger?: PluginLogger): ModelRegexFilter {
|
|
108
|
+
return {
|
|
109
|
+
includeRegex: (config.models?.includeRegex || []).map((pattern) => toRegExp(pattern, logger)).filter((pattern): pattern is RegExp => pattern !== null),
|
|
110
|
+
excludeRegex: (config.models?.excludeRegex || []).map((pattern) => toRegExp(pattern, logger)).filter((pattern): pattern is RegExp => pattern !== null),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
export function shouldDiscoverModel(modelId: string, filter: ModelRegexFilter): boolean {
|
|
82
115
|
if (filter.includeRegex.length > 0) {
|
|
83
116
|
return filter.includeRegex.some((pattern) => pattern.test(modelId))
|
package/src/ui/toast-notifier.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// UI notification system for
|
|
1
|
+
// UI notification system for the model discovery plugin
|
|
2
2
|
export class ToastNotifier {
|
|
3
3
|
private client: any // OpenCode client
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ export class ToastNotifier {
|
|
|
10
10
|
async success(message: string, title?: string, duration?: number): Promise<void> {
|
|
11
11
|
try {
|
|
12
12
|
if (!this.client?.tui?.showToast) {
|
|
13
|
-
console.warn('[opencode-
|
|
13
|
+
console.warn('[opencode-models-discovery] Toast API not available (client.tui.showToast missing)')
|
|
14
14
|
return
|
|
15
15
|
}
|
|
16
16
|
await this.client.tui.showToast({
|
|
@@ -22,7 +22,7 @@ export class ToastNotifier {
|
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
24
|
} catch (error) {
|
|
25
|
-
console.error(`[opencode-
|
|
25
|
+
console.error(`[opencode-models-discovery] Failed to show success toast`, error)
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ export class ToastNotifier {
|
|
|
30
30
|
async error(message: string, title?: string, duration?: number): Promise<void> {
|
|
31
31
|
try {
|
|
32
32
|
if (!this.client?.tui?.showToast) {
|
|
33
|
-
console.warn('[opencode-
|
|
33
|
+
console.warn('[opencode-models-discovery] Toast API not available (client.tui.showToast missing)')
|
|
34
34
|
return
|
|
35
35
|
}
|
|
36
36
|
await this.client.tui.showToast({
|
|
@@ -42,7 +42,7 @@ export class ToastNotifier {
|
|
|
42
42
|
}
|
|
43
43
|
})
|
|
44
44
|
} catch (error) {
|
|
45
|
-
console.error(`[opencode-
|
|
45
|
+
console.error(`[opencode-models-discovery] Failed to show error toast`, error)
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -50,7 +50,7 @@ export class ToastNotifier {
|
|
|
50
50
|
async warning(message: string, title?: string, duration?: number): Promise<void> {
|
|
51
51
|
try {
|
|
52
52
|
if (!this.client?.tui?.showToast) {
|
|
53
|
-
console.warn('[opencode-
|
|
53
|
+
console.warn('[opencode-models-discovery] Toast API not available (client.tui.showToast missing)')
|
|
54
54
|
return
|
|
55
55
|
}
|
|
56
56
|
await this.client.tui.showToast({
|
|
@@ -62,7 +62,7 @@ export class ToastNotifier {
|
|
|
62
62
|
}
|
|
63
63
|
})
|
|
64
64
|
} catch (error) {
|
|
65
|
-
console.error(`[opencode-
|
|
65
|
+
console.error(`[opencode-models-discovery] Failed to show warning toast`, error)
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -70,7 +70,7 @@ export class ToastNotifier {
|
|
|
70
70
|
async info(message: string, title?: string, duration?: number): Promise<void> {
|
|
71
71
|
try {
|
|
72
72
|
if (!this.client?.tui?.showToast) {
|
|
73
|
-
console.warn('[opencode-
|
|
73
|
+
console.warn('[opencode-models-discovery] Toast API not available (client.tui.showToast missing)')
|
|
74
74
|
return
|
|
75
75
|
}
|
|
76
76
|
await this.client.tui.showToast({
|
|
@@ -82,7 +82,7 @@ export class ToastNotifier {
|
|
|
82
82
|
}
|
|
83
83
|
})
|
|
84
84
|
} catch (error) {
|
|
85
|
-
console.error(`[opencode-
|
|
85
|
+
console.error(`[opencode-models-discovery] Failed to show info toast`, error)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -90,7 +90,7 @@ export class ToastNotifier {
|
|
|
90
90
|
async progress(message: string, title?: string, progress?: number): Promise<void> {
|
|
91
91
|
try {
|
|
92
92
|
if (!this.client?.tui?.showToast) {
|
|
93
|
-
console.warn('[opencode-
|
|
93
|
+
console.warn('[opencode-models-discovery] Toast API not available (client.tui.showToast missing)')
|
|
94
94
|
return
|
|
95
95
|
}
|
|
96
96
|
await this.client.tui.showToast({
|
|
@@ -102,7 +102,7 @@ export class ToastNotifier {
|
|
|
102
102
|
}
|
|
103
103
|
})
|
|
104
104
|
} catch (error) {
|
|
105
|
-
console.error(`[opencode-
|
|
105
|
+
console.error(`[opencode-models-discovery] Failed to show progress toast`, error)
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -115,7 +115,7 @@ export class ToastNotifier {
|
|
|
115
115
|
}): Promise<void> {
|
|
116
116
|
try {
|
|
117
117
|
if (!this.client?.tui?.showToast) {
|
|
118
|
-
console.warn('[opencode-
|
|
118
|
+
console.warn('[opencode-models-discovery] Toast API not available (client.tui.showToast missing)')
|
|
119
119
|
return
|
|
120
120
|
}
|
|
121
121
|
await this.client.tui.showToast({
|
|
@@ -127,7 +127,7 @@ export class ToastNotifier {
|
|
|
127
127
|
}
|
|
128
128
|
})
|
|
129
129
|
} catch (error) {
|
|
130
|
-
console.error(`[opencode-
|
|
130
|
+
console.error(`[opencode-models-discovery] Failed to show detailed toast`, error)
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
}
|
|
133
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ModelValidationError, AutoFixSuggestion, SimilarModel } from '../types'
|
|
2
|
-
|
|
3
1
|
export { formatModelName, extractModelOwner } from './format-model-name'
|
|
4
2
|
|
|
5
3
|
// Categorize models by type
|
|
@@ -16,201 +14,3 @@ export function categorizeModel(modelId: string): 'chat' | 'embedding' | 'unknow
|
|
|
16
14
|
}
|
|
17
15
|
return 'unknown'
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
// Enhanced model similarity matching
|
|
21
|
-
export function findSimilarModels(targetModel: string, availableModels: string[]): SimilarModel[] {
|
|
22
|
-
const target = targetModel.toLowerCase()
|
|
23
|
-
const targetTokens = target.split(/[-_\s]/).filter(Boolean)
|
|
24
|
-
|
|
25
|
-
return availableModels
|
|
26
|
-
.map(model => {
|
|
27
|
-
const candidate = model.toLowerCase()
|
|
28
|
-
const candidateTokens = candidate.split(/[-_\s]/).filter(Boolean)
|
|
29
|
-
|
|
30
|
-
let similarity = 0
|
|
31
|
-
const reasons: string[] = []
|
|
32
|
-
|
|
33
|
-
// Exact match gets highest score
|
|
34
|
-
if (candidate === target) {
|
|
35
|
-
similarity = 1.0
|
|
36
|
-
reasons.push("Exact match")
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Check for common model family prefixes
|
|
40
|
-
const targetPrefix = targetTokens[0]
|
|
41
|
-
const candidatePrefix = candidateTokens[0]
|
|
42
|
-
if (targetPrefix && candidatePrefix && targetPrefix === candidatePrefix) {
|
|
43
|
-
similarity += 0.5
|
|
44
|
-
reasons.push(`Same family: ${targetPrefix}`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check for common suffixes (quantization levels, sizes)
|
|
48
|
-
const commonSuffixes = ['3b', '7b', '13b', '70b', 'q4', 'q8', 'instruct', 'chat', 'base']
|
|
49
|
-
for (const suffix of commonSuffixes) {
|
|
50
|
-
if (target.includes(suffix) && candidate.includes(suffix)) {
|
|
51
|
-
similarity += 0.2
|
|
52
|
-
reasons.push(`Shared suffix: ${suffix}`)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Token overlap score
|
|
57
|
-
const commonTokens = targetTokens.filter(token => candidateTokens.includes(token))
|
|
58
|
-
if (commonTokens.length > 0) {
|
|
59
|
-
similarity += (commonTokens.length / Math.max(targetTokens.length, candidateTokens.length)) * 0.3
|
|
60
|
-
reasons.push(`Common tokens: ${commonTokens.join(', ')}`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
model,
|
|
65
|
-
similarity: Math.min(similarity, 1.0),
|
|
66
|
-
reason: reasons.join(", ")
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
.filter(item => item.similarity > 0.1) // Only include models with some similarity
|
|
70
|
-
.sort((a, b) => b.similarity - a.similarity)
|
|
71
|
-
.slice(0, 5) // Top 5 suggestions
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Retry logic with exponential backoff
|
|
75
|
-
export async function retryWithBackoff<T>(
|
|
76
|
-
operation: () => Promise<T>,
|
|
77
|
-
maxRetries: number = 3,
|
|
78
|
-
baseDelay: number = 1000
|
|
79
|
-
): Promise<{ success: boolean; result?: T; error?: string }> {
|
|
80
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
81
|
-
try {
|
|
82
|
-
const result = await operation()
|
|
83
|
-
return { success: true, result }
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (attempt === maxRetries) {
|
|
86
|
-
return {
|
|
87
|
-
success: false,
|
|
88
|
-
error: error instanceof Error ? error.message : String(error)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const delay = baseDelay * Math.pow(2, attempt)
|
|
93
|
-
console.warn(`[opencode-model-discovery] Retrying operation after ${delay}ms`, {
|
|
94
|
-
attempt: attempt + 1,
|
|
95
|
-
maxRetries: maxRetries + 1,
|
|
96
|
-
error: error instanceof Error ? error.message : String(error)
|
|
97
|
-
})
|
|
98
|
-
await new Promise(resolve => setTimeout(resolve, delay))
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return { success: false, error: "Max retries exceeded" }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Smart error categorization
|
|
105
|
-
export function categorizeError(error: any, context: { baseURL: string; modelId: string }): ModelValidationError {
|
|
106
|
-
const errorStr = String(error).toLowerCase()
|
|
107
|
-
const { baseURL, modelId } = context
|
|
108
|
-
|
|
109
|
-
// Network/connection issues
|
|
110
|
-
if (errorStr.includes('econnrefused') || errorStr.includes('fetch failed') || errorStr.includes('network')) {
|
|
111
|
-
return {
|
|
112
|
-
type: 'offline',
|
|
113
|
-
severity: 'critical',
|
|
114
|
-
message: `Cannot connect to provider at ${baseURL}. Ensure the server is running and accessible.`,
|
|
115
|
-
canRetry: true,
|
|
116
|
-
autoFixAvailable: true
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Timeout issues
|
|
121
|
-
if (errorStr.includes('timeout') || errorStr.includes('aborted')) {
|
|
122
|
-
return {
|
|
123
|
-
type: 'timeout',
|
|
124
|
-
severity: 'medium',
|
|
125
|
-
message: `Request timed out. This might happen with large models or slow systems.`,
|
|
126
|
-
canRetry: true,
|
|
127
|
-
autoFixAvailable: false
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Model not found
|
|
132
|
-
if (errorStr.includes('404') || errorStr.includes('not found')) {
|
|
133
|
-
return {
|
|
134
|
-
type: 'not_found',
|
|
135
|
-
severity: 'high',
|
|
136
|
-
message: `Model '${modelId}' not found. Check if model is available in the provider.`,
|
|
137
|
-
canRetry: false,
|
|
138
|
-
autoFixAvailable: false
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Permission issues
|
|
143
|
-
if (errorStr.includes('401') || errorStr.includes('403') || errorStr.includes('unauthorized')) {
|
|
144
|
-
return {
|
|
145
|
-
type: 'permission',
|
|
146
|
-
severity: 'high',
|
|
147
|
-
message: `Authentication or permission issue. Check your provider configuration.`,
|
|
148
|
-
canRetry: false,
|
|
149
|
-
autoFixAvailable: false
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Unknown errors
|
|
154
|
-
return {
|
|
155
|
-
type: 'unknown',
|
|
156
|
-
severity: 'medium',
|
|
157
|
-
message: `Unexpected error: ${errorStr}`,
|
|
158
|
-
canRetry: true,
|
|
159
|
-
autoFixAvailable: false
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Generate auto-fix suggestions
|
|
164
|
-
export function generateAutoFixSuggestions(errorCategory: ModelValidationError): AutoFixSuggestion[] {
|
|
165
|
-
const suggestions: AutoFixSuggestion[] = []
|
|
166
|
-
|
|
167
|
-
switch (errorCategory.type) {
|
|
168
|
-
case 'offline':
|
|
169
|
-
suggestions.push({
|
|
170
|
-
action: "Check if provider server is running",
|
|
171
|
-
steps: [
|
|
172
|
-
"1. Verify the server application is running",
|
|
173
|
-
"2. Verify the server is started",
|
|
174
|
-
"3. Check the server URL and port",
|
|
175
|
-
"4. Ensure the server is not blocked by firewall"
|
|
176
|
-
],
|
|
177
|
-
automated: false
|
|
178
|
-
})
|
|
179
|
-
suggestions.push({
|
|
180
|
-
action: "Try alternative ports",
|
|
181
|
-
steps: [
|
|
182
|
-
"1. Check if provider is running on a different port",
|
|
183
|
-
"2. Common ports: 1234 (LM Studio), 8080 (LocalAI), 11434 (Ollama)",
|
|
184
|
-
"3. Update your OpenCode configuration with the correct port"
|
|
185
|
-
],
|
|
186
|
-
automated: false
|
|
187
|
-
})
|
|
188
|
-
break
|
|
189
|
-
|
|
190
|
-
case 'not_found':
|
|
191
|
-
suggestions.push({
|
|
192
|
-
action: "Check model availability",
|
|
193
|
-
steps: [
|
|
194
|
-
"1. Verify the model is available in your provider",
|
|
195
|
-
"2. Download or load the model if needed",
|
|
196
|
-
"3. Ensure the model is properly installed"
|
|
197
|
-
],
|
|
198
|
-
automated: false
|
|
199
|
-
})
|
|
200
|
-
break
|
|
201
|
-
|
|
202
|
-
case 'timeout':
|
|
203
|
-
suggestions.push({
|
|
204
|
-
action: "Increase timeout or use smaller model",
|
|
205
|
-
steps: [
|
|
206
|
-
"1. Try a smaller model version",
|
|
207
|
-
"2. Increase request timeout in OpenCode settings",
|
|
208
|
-
"3. Close other applications to free up system resources"
|
|
209
|
-
],
|
|
210
|
-
automated: false
|
|
211
|
-
})
|
|
212
|
-
break
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return suggestions
|
|
216
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { CacheStats } from '../types'
|
|
2
|
-
|
|
3
|
-
// Model Status Cache for reducing API calls
|
|
4
|
-
export class ModelStatusCache {
|
|
5
|
-
private cache = new Map<string, {
|
|
6
|
-
models: string[]
|
|
7
|
-
timestamp: number
|
|
8
|
-
ttl: number
|
|
9
|
-
}>()
|
|
10
|
-
|
|
11
|
-
private readonly DEFAULT_TTL = 15000 // 15 seconds (reduced for better freshness)
|
|
12
|
-
private readonly MAX_CACHE_SIZE = 50 // Prevent memory leaks
|
|
13
|
-
|
|
14
|
-
// Get cached model status or fetch fresh data
|
|
15
|
-
async getModels(baseURL: string, fetchFn: () => Promise<string[]>): Promise<string[]> {
|
|
16
|
-
const now = Date.now()
|
|
17
|
-
const cached = this.cache.get(baseURL)
|
|
18
|
-
|
|
19
|
-
// Return cached data if still valid
|
|
20
|
-
if (cached && (now - cached.timestamp) < cached.ttl) {
|
|
21
|
-
return cached.models
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Fetch fresh data
|
|
25
|
-
try {
|
|
26
|
-
const models = await fetchFn()
|
|
27
|
-
|
|
28
|
-
// Update cache with new data
|
|
29
|
-
this.cache.set(baseURL, {
|
|
30
|
-
models: [...models], // Create copy to prevent mutations
|
|
31
|
-
timestamp: now,
|
|
32
|
-
ttl: this.DEFAULT_TTL
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
// Prevent cache from growing too large
|
|
36
|
-
if (this.cache.size > this.MAX_CACHE_SIZE) {
|
|
37
|
-
this.cleanup()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return models
|
|
42
|
-
} catch (error) {
|
|
43
|
-
// If we have stale cached data, return it as fallback but mark as potentially invalid
|
|
44
|
-
if (cached) {
|
|
45
|
-
console.warn(`[opencode-model-discovery] Using stale cache data due to fetch error`, {
|
|
46
|
-
baseURL,
|
|
47
|
-
age: now - cached.timestamp,
|
|
48
|
-
error: error instanceof Error ? error.message : String(error)
|
|
49
|
-
})
|
|
50
|
-
// Invalidate cache if it's very old (> 5x TTL)
|
|
51
|
-
if (now - cached.timestamp > cached.ttl * 5) {
|
|
52
|
-
this.invalidate(baseURL)
|
|
53
|
-
}
|
|
54
|
-
return cached.models
|
|
55
|
-
}
|
|
56
|
-
throw error
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Invalidate cache for specific URL
|
|
61
|
-
invalidate(baseURL: string): void {
|
|
62
|
-
this.cache.delete(baseURL)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Invalidate entire cache
|
|
66
|
-
invalidateAll(): void {
|
|
67
|
-
this.cache.clear()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Force refresh for specific URL (useful after model changes)
|
|
71
|
-
async forceRefresh(baseURL: string, fetchFn: () => Promise<string[]>): Promise<string[]> {
|
|
72
|
-
this.invalidate(baseURL)
|
|
73
|
-
return this.getModels(baseURL, fetchFn)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Get cache statistics
|
|
77
|
-
getStats(): CacheStats {
|
|
78
|
-
const now = Date.now()
|
|
79
|
-
return {
|
|
80
|
-
size: this.cache.size,
|
|
81
|
-
entries: Array.from(this.cache.entries()).map(([baseURL, data]) => ({
|
|
82
|
-
baseURL,
|
|
83
|
-
age: now - data.timestamp,
|
|
84
|
-
modelCount: data.models.length,
|
|
85
|
-
ttl: data.ttl
|
|
86
|
-
}))
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Cleanup old entries to prevent memory leaks
|
|
91
|
-
private cleanup(): void {
|
|
92
|
-
const now = Date.now()
|
|
93
|
-
const toDelete: string[] = []
|
|
94
|
-
|
|
95
|
-
for (const [baseURL, data] of this.cache.entries()) {
|
|
96
|
-
// Delete entries older than 5x TTL or if cache is too large
|
|
97
|
-
if (now - data.timestamp > data.ttl * 5 || this.cache.size > this.MAX_CACHE_SIZE) {
|
|
98
|
-
toDelete.push(baseURL)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
toDelete.forEach(baseURL => this.cache.delete(baseURL))
|
|
103
|
-
|
|
104
|
-
if (toDelete.length > 0) {
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Configure TTL for specific use cases
|
|
109
|
-
setTTL(baseURL: string, ttl: number): void {
|
|
110
|
-
const cached = this.cache.get(baseURL)
|
|
111
|
-
if (cached) {
|
|
112
|
-
cached.ttl = ttl
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Check if cache entry exists and is valid
|
|
117
|
-
isValid(baseURL: string): boolean {
|
|
118
|
-
const cached = this.cache.get(baseURL)
|
|
119
|
-
const now = Date.now()
|
|
120
|
-
return cached !== undefined && (now - cached.timestamp) < cached.ttl
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { ModelStatusCache } from '../cache/model-status-cache'
|
|
2
|
-
import { fetchModelsDirect } from '../utils/openai-compatible-api'
|
|
3
|
-
|
|
4
|
-
const modelStatusCache = new ModelStatusCache()
|
|
5
|
-
|
|
6
|
-
export function getLoadedModels(baseURL: string): Promise<string[]> {
|
|
7
|
-
return modelStatusCache.getModels(baseURL, async () => {
|
|
8
|
-
return await fetchModelsDirect(baseURL)
|
|
9
|
-
})
|
|
10
|
-
}
|