opencode-models-discovery 0.4.9 → 0.5.3

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 CHANGED
@@ -1,5 +1,8 @@
1
1
  # opencode-models-discovery
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/opencode-models-discovery.svg?color=blue)](https://www.npmjs.com/package/opencode-models-discovery)
4
+ [![npm downloads](https://img.shields.io/npm/dt/opencode-models-discovery.svg)](https://www.npmjs.com/package/opencode-models-discovery)
5
+
3
6
  > Forked from [opencode-lmstudio](https://github.com/nicktasios/opencode-lmstudio) and expanded to support **any OpenAI-compatible provider**.
4
7
 
5
8
  OpenCode plugin for auto-discovery of OpenAI-compatible models with dynamic provider configuration.
@@ -66,6 +69,10 @@ The plugin configuration is placed in the `plugin` array using tuple format `["p
66
69
  "include": [],
67
70
  "exclude": []
68
71
  },
72
+ "models": {
73
+ "includeRegex": [],
74
+ "excludeRegex": []
75
+ },
69
76
  "discovery": {
70
77
  "enabled": true,
71
78
  "ttl": 15000
@@ -97,6 +104,30 @@ Control which providers are discovered:
97
104
  }
98
105
  ```
99
106
 
107
+ #### Model Filtering
108
+
109
+ Control which discovered models are auto-injected with regular expressions:
110
+
111
+ | Option | Type | Description |
112
+ |--------|------|-------------|
113
+ | `models.includeRegex` | `string[]` | If non-empty, only discovered model IDs matching at least one regex will be added |
114
+ | `models.excludeRegex` | `string[]` | Discovered model IDs matching any regex will be skipped (only used when `includeRegex` is empty) |
115
+
116
+ Regex filtering only applies to auto-discovered models. Models already explicitly configured by the user are preserved.
117
+
118
+ ```json
119
+ {
120
+ "plugin": [
121
+ ["opencode-models-discovery", {
122
+ "models": {
123
+ "includeRegex": ["^qwen/", "gpt-4"],
124
+ "excludeRegex": ["embedding", "test"]
125
+ }
126
+ }]
127
+ ]
128
+ }
129
+ ```
130
+
100
131
  ### How It Works
101
132
 
102
133
  1. On OpenCode startup, the plugin's `config` hook is called
@@ -119,7 +150,7 @@ The plugin supports any OpenAI-compatible provider. Here are the most common one
119
150
  | **Text Generation WebUI** | 5000 | OpenAI-compatible extension | `@ai-sdk/openai-compatible` |
120
151
  | **FastChat (Vicuna)** | 8001 | Multi-model serving | `@ai-sdk/openai-compatible` |
121
152
  | **vLLM** | 8000 | High-performance inference | `@ai-sdk/openai-compatible` |
122
- | **Anysphere (Cursor)** | | IDE with AI completion | `@ai-sdk/anthropic` (with `/v1` backend) |
153
+ | **CLIProxyAPI** | 8317 | A LLM proxy server | `@ai-sdk/anthropic` (with `/v1` backend) & `@ai-sdk/openai-compatible` |
123
154
 
124
155
  #### Anthropic API with Custom Backend
125
156
 
@@ -203,10 +234,16 @@ This means providers using `@ai-sdk/anthropic` with OpenAI-compatible backends (
203
234
  - At least one OpenAI-compatible provider running locally or remotely
204
235
  - Provider server API accessible (e.g., `http://127.0.0.1:11434/v1`)
205
236
 
237
+ ## Logging
238
+
239
+ When available, the plugin writes logs through OpenCode's structured server log API via `client.app.log(...)` using the service name `opencode-model-discovery`.
240
+
241
+ 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`.
242
+
206
243
  ## License
207
244
 
208
245
  MIT
209
246
 
210
247
  ## Contributing
211
248
 
212
- Contributions are welcome! Please feel free to submit a Pull Request.
249
+ Contributions are welcome! Please feel free to submit a Pull Request.
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.9",
4
+ "version": "0.5.3",
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",
@@ -1,38 +1,45 @@
1
1
  import { ToastNotifier } from '../ui/toast-notifier'
2
2
  import { validateConfig } from '../utils/validation'
3
3
  import { enhanceConfig } from './enhance-config'
4
+ import type { PluginLogger } from './logger'
4
5
  import type { PluginInput } from '@opencode-ai/plugin'
5
6
  import type { PluginConfig } from '../types/plugin-config'
6
7
 
7
8
  export function createConfigHook(
8
9
  client: PluginInput['client'],
9
10
  toastNotifier: ToastNotifier,
10
- pluginConfig: PluginConfig
11
+ pluginConfig: PluginConfig,
12
+ logger: PluginLogger
11
13
  ) {
12
14
  return async (config: any) => {
13
15
  if (config && (Object.isFrozen?.(config) || Object.isSealed?.(config))) {
14
- console.warn("[opencode-model-discovery] Config object is frozen/sealed - cannot modify directly")
16
+ logger.warn('Config object is frozen or sealed; cannot modify directly')
15
17
  return
16
18
  }
17
19
 
18
20
  const validation = validateConfig(config)
19
21
  if (!validation.isValid) {
20
- console.error("[opencode-model-discovery] Invalid config provided:", validation.errors)
22
+ logger.error('Invalid config provided', { errors: validation.errors })
21
23
  toastNotifier.error("Plugin configuration is invalid", "Configuration Error").catch(() => {})
22
24
  return
23
25
  }
24
26
 
25
27
  if (validation.warnings.length > 0) {
26
- console.warn("[opencode-model-discovery] Config warnings:", validation.warnings)
28
+ logger.warn('Config warnings', { warnings: validation.warnings })
27
29
  }
28
30
 
29
31
  if (pluginConfig.discovery?.enabled === false) {
30
- console.log("[opencode-model-discovery] Discovery disabled by configuration")
32
+ logger.info('Discovery disabled by configuration')
31
33
  return
32
34
  }
33
35
 
34
- const startTime = Date.now()
35
- const discoveryPromise = enhanceConfig(config, client, toastNotifier, pluginConfig)
36
+ const discoveryPromise = enhanceConfig(
37
+ config,
38
+ client,
39
+ toastNotifier,
40
+ pluginConfig,
41
+ logger.child({ category: 'discovery' })
42
+ )
36
43
  const timeoutMs = 5000
37
44
 
38
45
  try {
@@ -43,7 +50,9 @@ export function createConfigHook(
43
50
  })
44
51
  ])
45
52
  } catch (error) {
46
- console.error("[opencode-model-discovery] Config enhancement failed:", error)
53
+ logger.error('Config enhancement failed', {
54
+ error: error instanceof Error ? error.message : String(error),
55
+ })
47
56
  }
48
57
  }
49
- }
58
+ }
@@ -2,7 +2,8 @@ import { ModelStatusCache } from '../cache/model-status-cache'
2
2
  import { ToastNotifier } from '../ui/toast-notifier'
3
3
  import { categorizeModel, formatModelName, extractModelOwner } from '../utils'
4
4
  import { normalizeBaseURL, checkProviderHealth, discoverModelsFromProvider, autoDetectOpenAICompatibleProvider, canDiscoverModels } from '../utils/openai-compatible-api'
5
- import { getProviderFilter, getDiscoveryConfig, shouldDiscoverProvider } from '../types/plugin-config'
5
+ import { getProviderFilter, getDiscoveryConfig, getModelRegexFilter, shouldDiscoverModel, shouldDiscoverProvider } from '../types/plugin-config'
6
+ import type { PluginLogger } from './logger'
6
7
  import type { PluginInput } from '@opencode-ai/plugin'
7
8
  import type { OpenAIModel } from '../types'
8
9
  import type { PluginConfig } from '../types/plugin-config'
@@ -19,7 +20,8 @@ export async function enhanceConfig(
19
20
  config: any,
20
21
  client: PluginInput['client'],
21
22
  toastNotifier: ToastNotifier,
22
- pluginConfig: PluginConfig
23
+ pluginConfig: PluginConfig,
24
+ logger: PluginLogger
23
25
  ): Promise<void> {
24
26
  modelStatusCache.invalidateAll()
25
27
 
@@ -27,6 +29,7 @@ export async function enhanceConfig(
27
29
  const providers = config.provider || {}
28
30
  const openAICompatibleProviders: DiscoveredProvider[] = []
29
31
  const providerFilter = getProviderFilter(pluginConfig)
32
+ const modelRegexFilter = getModelRegexFilter(pluginConfig, logger.child({ category: 'filtering' }))
30
33
  const discoveryConfig = getDiscoveryConfig(pluginConfig)
31
34
 
32
35
  for (const [providerName, providerConfig] of Object.entries(providers)) {
@@ -61,6 +64,11 @@ export async function enhanceConfig(
61
64
  try {
62
65
  models = await discoverModelsFromProvider(baseURL, apiKey)
63
66
  } catch (error) {
67
+ logger.warn('Provider model discovery failed', {
68
+ provider: providerName,
69
+ baseURL,
70
+ error: error instanceof Error ? error.message : String(error),
71
+ })
64
72
  continue
65
73
  }
66
74
 
@@ -77,6 +85,10 @@ export async function enhanceConfig(
77
85
  const modelKey = model.id
78
86
 
79
87
  if (!existingModels[modelKey]) {
88
+ if (!shouldDiscoverModel(model.id, modelRegexFilter)) {
89
+ continue
90
+ }
91
+
80
92
  const modelType = categorizeModel(model.id)
81
93
  const owner = extractModelOwner(model.id)
82
94
  const modelConfig: any = {
@@ -126,13 +138,19 @@ export async function enhanceConfig(
126
138
 
127
139
  if (openAICompatibleProviders.length > 0) {
128
140
  const totalModels = openAICompatibleProviders.reduce((sum, p) => sum + Object.keys(p.models).length, 0)
129
- // Discovery complete - models are now available
141
+ logger.info('Provider model discovery completed', {
142
+ providerCount: openAICompatibleProviders.length,
143
+ modelCount: totalModels,
144
+ })
130
145
  }
131
146
 
132
147
  if (Object.keys(providers).length === 0) {
133
148
  const detected = await autoDetectOpenAICompatibleProvider()
134
149
  if (detected) {
135
- // Auto-detection found a provider, but no config exists
150
+ logger.info('Detected OpenAI-compatible provider but found no configured providers', {
151
+ provider: detected.name,
152
+ baseURL: detected.baseURL,
153
+ })
136
154
  }
137
155
  }
138
156
 
@@ -155,9 +173,14 @@ export async function enhanceConfig(
155
173
  }
156
174
  }
157
175
  } catch (error) {
176
+ logger.warn('Model status cache refresh failed', {
177
+ error: error instanceof Error ? error.message : String(error),
178
+ })
158
179
  }
159
180
  } catch (error) {
160
- console.error("[opencode-model-discovery] Unexpected error in enhanceConfig:", error)
181
+ logger.error('Unexpected error in enhanceConfig', {
182
+ error: error instanceof Error ? error.message : String(error),
183
+ })
161
184
  toastNotifier.warning("Plugin configuration failed", "Configuration Error").catch(() => {})
162
185
  }
163
- }
186
+ }
@@ -1,18 +1,16 @@
1
1
  import { validateHookInput } from '../utils/validation'
2
+ import type { PluginLogger } from './logger'
2
3
 
3
- export function createEventHook() {
4
+ export function createEventHook(logger: PluginLogger) {
4
5
  return async ({ event }: { event: any }) => {
5
- // Validate event input
6
6
  const validation = validateHookInput('event', { event })
7
7
  if (!validation.isValid) {
8
- console.error("[opencode-model-discovery] Invalid event input:", validation.errors)
8
+ logger.error('Invalid event input', { errors: validation.errors })
9
9
  return
10
10
  }
11
11
 
12
- // Monitor for session events to provide LM Studio status
13
12
  if (event.type === "session.created" || event.type === "session.updated") {
14
- // Could add health check monitoring here in the future
13
+ // Reserved for future session-aware discovery diagnostics.
15
14
  }
16
15
  }
17
16
  }
18
-
@@ -3,15 +3,15 @@ import { ToastNotifier } from '../ui/toast-notifier'
3
3
  import { createConfigHook } from './config-hook'
4
4
  import { createEventHook } from './event-hook'
5
5
  import { createChatParamsHook } from './chat-params-hook'
6
+ import { createPluginLogger } from './logger'
6
7
  import { parsePluginConfig, type PluginConfig } from '../types/plugin-config'
7
8
 
8
9
  export const ModelDiscoveryPlugin: Plugin = async (input: PluginInput, options?: PluginOptions) => {
9
- console.log("[opencode-model-discovery] Model discovery plugin initialized")
10
-
11
10
  const { client } = input
11
+ const logger = createPluginLogger(client, { category: 'plugin' })
12
12
 
13
13
  if (!client || typeof client !== 'object') {
14
- console.error("[opencode-model-discovery] Invalid client provided to plugin")
14
+ logger.error('Invalid client provided to plugin')
15
15
  return {
16
16
  config: async () => {},
17
17
  event: async () => {},
@@ -19,19 +19,21 @@ export const ModelDiscoveryPlugin: Plugin = async (input: PluginInput, options?:
19
19
  }
20
20
  }
21
21
 
22
+ logger.info('Model discovery plugin initialized')
23
+
22
24
  const pluginConfig: PluginConfig = parsePluginConfig(options || {})
23
25
 
24
26
  if (pluginConfig.discovery?.enabled === false) {
25
- console.log("[opencode-model-discovery] Discovery disabled by configuration")
27
+ logger.info('Discovery disabled by configuration', { category: 'config' })
26
28
  }
27
29
 
28
30
  const toastNotifier = new ToastNotifier(client)
29
31
 
30
32
  return {
31
- config: createConfigHook(client, toastNotifier, pluginConfig),
32
- event: createEventHook(),
33
+ config: createConfigHook(client, toastNotifier, pluginConfig, logger.child({ category: 'config' })),
34
+ event: createEventHook(logger.child({ category: 'event' })),
33
35
  "chat.params": createChatParamsHook(toastNotifier, pluginConfig),
34
36
  }
35
37
  }
36
38
 
37
- export const LMStudioPlugin = ModelDiscoveryPlugin
39
+ export const LMStudioPlugin = ModelDiscoveryPlugin
@@ -0,0 +1,98 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin'
2
+
3
+ const SERVICE_NAME = 'opencode-models-discovery'
4
+
5
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error'
6
+
7
+ type LogExtra = Record<string, unknown>
8
+
9
+ type PluginClient = PluginInput['client'] | null | undefined
10
+
11
+ export interface PluginLogger {
12
+ debug(message: string, extra?: LogExtra): void
13
+ info(message: string, extra?: LogExtra): void
14
+ warn(message: string, extra?: LogExtra): void
15
+ error(message: string, extra?: LogExtra): void
16
+ child(extra: LogExtra): PluginLogger
17
+ }
18
+
19
+ function getConsoleMethod(level: LogLevel): typeof console.log {
20
+ if (level === 'error') {
21
+ return console.error
22
+ }
23
+
24
+ if (level === 'warn') {
25
+ return console.warn
26
+ }
27
+
28
+ if (level === 'debug') {
29
+ return console.debug
30
+ }
31
+
32
+ return console.info
33
+ }
34
+
35
+ function mergeExtra(baseExtra: LogExtra, extra?: LogExtra): LogExtra | undefined {
36
+ const merged = {
37
+ ...baseExtra,
38
+ ...extra,
39
+ }
40
+
41
+ return Object.keys(merged).length > 0 ? merged : undefined
42
+ }
43
+
44
+ function fallbackToConsole(level: LogLevel, message: string, extra?: LogExtra) {
45
+ const log = getConsoleMethod(level)
46
+ const prefix = `[${SERVICE_NAME}] ${message}`
47
+
48
+ if (extra && Object.keys(extra).length > 0) {
49
+ log(prefix, extra)
50
+ return
51
+ }
52
+
53
+ log(prefix)
54
+ }
55
+
56
+ export function createPluginLogger(client?: PluginClient, baseExtra: LogExtra = {}): PluginLogger {
57
+ const log = (level: LogLevel, message: string, extra?: LogExtra) => {
58
+ const mergedExtra = mergeExtra(baseExtra, extra)
59
+
60
+ try {
61
+ if (client?.app?.log) {
62
+ void client.app.log({
63
+ body: {
64
+ service: SERVICE_NAME,
65
+ level,
66
+ message,
67
+ extra: mergedExtra,
68
+ },
69
+ }).catch(() => {
70
+ fallbackToConsole(level, message, mergedExtra)
71
+ })
72
+ return
73
+ }
74
+ } catch {
75
+ // Fall back to console logging when structured logging is unavailable.
76
+ }
77
+
78
+ fallbackToConsole(level, message, mergedExtra)
79
+ }
80
+
81
+ return {
82
+ debug(message, extra) {
83
+ log('debug', message, extra)
84
+ },
85
+ info(message, extra) {
86
+ log('info', message, extra)
87
+ },
88
+ warn(message, extra) {
89
+ log('warn', message, extra)
90
+ },
91
+ error(message, extra) {
92
+ log('error', message, extra)
93
+ },
94
+ child(extra) {
95
+ return createPluginLogger(client, mergeExtra(baseExtra, extra) || {})
96
+ },
97
+ }
98
+ }
@@ -3,6 +3,10 @@ export interface PluginConfig {
3
3
  include?: string[]
4
4
  exclude?: string[]
5
5
  }
6
+ models?: {
7
+ includeRegex?: string[]
8
+ excludeRegex?: string[]
9
+ }
6
10
  discovery?: {
7
11
  enabled?: boolean
8
12
  ttl?: number
@@ -19,6 +23,11 @@ export interface DiscoveryConfig {
19
23
  ttl: number
20
24
  }
21
25
 
26
+ export interface ModelRegexFilter {
27
+ includeRegex: RegExp[]
28
+ excludeRegex: RegExp[]
29
+ }
30
+
22
31
  export const DEFAULT_DISCOVERY_CONFIG: DiscoveryConfig = {
23
32
  enabled: true,
24
33
  ttl: 15000,
@@ -48,6 +57,38 @@ export function getDiscoveryConfig(config: PluginConfig): DiscoveryConfig {
48
57
  }
49
58
  }
50
59
 
60
+ function toRegExp(pattern: string, logger?: PluginLogger): RegExp | null {
61
+ try {
62
+ return new RegExp(pattern)
63
+ } catch {
64
+ if (logger) {
65
+ logger.warn('Ignoring invalid model regex', { category: 'filtering', pattern })
66
+ } else {
67
+ console.warn(`[opencode-model-discovery] Ignoring invalid model regex: ${pattern}`)
68
+ }
69
+ return null
70
+ }
71
+ }
72
+
73
+ export function getModelRegexFilter(config: PluginConfig, logger?: PluginLogger): ModelRegexFilter {
74
+ return {
75
+ includeRegex: (config.models?.includeRegex || []).map((pattern) => toRegExp(pattern, logger)).filter((pattern): pattern is RegExp => pattern !== null),
76
+ excludeRegex: (config.models?.excludeRegex || []).map((pattern) => toRegExp(pattern, logger)).filter((pattern): pattern is RegExp => pattern !== null),
77
+ }
78
+ }
79
+
80
+ export function shouldDiscoverModel(modelId: string, filter: ModelRegexFilter): boolean {
81
+ if (filter.includeRegex.length > 0) {
82
+ return filter.includeRegex.some((pattern) => pattern.test(modelId))
83
+ }
84
+
85
+ if (filter.excludeRegex.length > 0) {
86
+ return !filter.excludeRegex.some((pattern) => pattern.test(modelId))
87
+ }
88
+
89
+ return true
90
+ }
91
+
51
92
  export function parsePluginConfig(rawConfig: any): PluginConfig {
52
93
  if (!rawConfig) {
53
94
  return {}
@@ -68,4 +109,5 @@ export function parsePluginConfig(rawConfig: any): PluginConfig {
68
109
  }
69
110
 
70
111
  return {}
71
- }
112
+ }
113
+ import type { PluginLogger } from '../plugin/logger'