Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Add the plugin to your `opencode.json`:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
// For dev testing, you can replace this with file:$YOUR_LOCAL_PLUGIN_REPO_PATH
// Example: file:/Users/you/workspace/opencode-lmstudio
"opencode-lmstudio@latest"
],
"provider": {
Expand Down Expand Up @@ -77,6 +79,40 @@ You can also manually configure the provider with specific models:

The plugin will automatically discover and add any additional models available in LM Studio that aren't already configured.

### Filter Models by `modelTypes`

Configure `modelTypes` under `provider.lmstudio.options.modelTypes`:

```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"opencode-lmstudio@latest"
],
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio (local)",
"options": {
"baseURL": "http://127.0.0.1:1234/v1",
"modelTypes": ["unknown"]
}
}
}
}
```

Allowed values:

- `chat`
- `embedding`
- `unknown`

Current behavior:

- `modelTypes` is derived from the LM Studio model `id`, not a dedicated API type field.
- `unknown` means the model `id` did not match the plugin's current `chat` or `embedding` rules.

## How It Works

1. On OpenCode startup, the plugin's `config` hook is called
Expand All @@ -99,4 +135,3 @@ MIT
## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { LMStudioPlugin } from './plugin'
export { LMStudioPlugin } from './plugin'
25 changes: 23 additions & 2 deletions src/plugin/enhance-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,25 @@ import { ToastNotifier } from '../ui/toast-notifier'
import { categorizeModel, formatModelName, extractModelOwner } from '../utils'
import { normalizeBaseURL, checkLMStudioHealth, discoverLMStudioModels, autoDetectLMStudio } from '../utils/lmstudio-api'
import type { PluginInput } from '@opencode-ai/plugin'
import type { LMStudioModel } from '../types'
import type { LMStudioModel, ModelType } from '../types'

const modelStatusCache = new ModelStatusCache()
const allowedModelTypes = ['chat', 'embedding', 'unknown'] as const

function getConfiguredModelTypes(lmstudioProvider: any): Set<ModelType> | null {
const modelTypes = lmstudioProvider?.options?.modelTypes ?? lmstudioProvider?.options?.model_types

if (!Array.isArray(modelTypes) || modelTypes.length === 0) {
return null
}

return new Set(
modelTypes.filter((modelType: unknown): modelType is ModelType =>
typeof modelType === 'string' &&
(allowedModelTypes as readonly string[]).includes(modelType)
)
)
}

export async function enhanceConfig(
config: any,
Expand Down Expand Up @@ -64,6 +80,7 @@ export async function enhanceConfig(
// Merge discovered models with configured models
const existingModels = lmstudioProvider.models || {}
const discoveredModels: Record<string, any> = {}
const configuredModelTypes = getConfiguredModelTypes(lmstudioProvider)
let chatModelsCount = 0
let embeddingModelsCount = 0

Expand All @@ -77,6 +94,11 @@ export async function enhanceConfig(
// Only add if not already configured
if (!existingModels[modelKey] && !existingModels[model.id]) {
const modelType = categorizeModel(model.id)

if (configuredModelTypes && !configuredModelTypes.has(modelType)) {
continue
}

const owner = extractModelOwner(model.id)
const modelConfig: any = {
id: model.id,
Expand Down Expand Up @@ -153,4 +175,3 @@ export async function enhanceConfig(
toastNotifier.warning("Plugin configuration failed", "Configuration Error").catch(() => {})
}
}

33 changes: 32 additions & 1 deletion src/utils/validation/validate-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import type { ValidationResult } from './validation-result'

const allowedModelTypes = new Set(['chat', 'embedding', 'unknown'])
const allowedModelTypesList = Array.from(allowedModelTypes).join(', ')

function formatInvalidModelTypeError(modelType: unknown): string {
const value = String(modelType)
const suggestion = value === 'embedded' ? ' Did you mean "embedding"?' : ''
return `LM Studio provider options.modelTypes contains invalid value: ${value}. Allowed values: ${allowedModelTypesList}.${suggestion}`
}

function validateModelTypesOption(
errors: string[],
modelTypes: unknown,
optionPath: string
): void {
if (!Array.isArray(modelTypes)) {
errors.push(`LM Studio provider ${optionPath} must be an array`)
return
}

for (const modelType of modelTypes) {
if (typeof modelType !== 'string' || !allowedModelTypes.has(modelType)) {
errors.push(formatInvalidModelTypeError(modelType))
}
}
}

export function validateConfig(config: any): ValidationResult {
const errors: string[] = []
const warnings: string[] = []
Expand Down Expand Up @@ -34,6 +60,12 @@ export function validateConfig(config: any): ValidationResult {
} else if (!isValidURL(lmstudio.options.baseURL)) {
warnings.push('LM Studio provider baseURL may be invalid')
}

if (lmstudio.options.modelTypes !== undefined) {
validateModelTypesOption(errors, lmstudio.options.modelTypes, 'options.modelTypes')
} else if (lmstudio.options.model_types !== undefined) {
validateModelTypesOption(errors, lmstudio.options.model_types, 'options.model_types')
}
}

// Validate models configuration
Expand All @@ -58,4 +90,3 @@ function isValidURL(url: string): boolean {
return false
}
}

Loading