Skip to content

Commit fdffd8b

Browse files
github-actions[bot]Riven-Spellyuhp
authored
Publish v1.1.0 (#48)
* Add vllm backend support (#47) * `npm audit fix` * Refactor to use an enum instead for ModelInfoFormat * Add support for vLLM-compatible-backed endpoints to provide model context length * Trim unnecessary tests * Remove input limit * Revert "`npm audit fix`" This reverts commit d5d98a2. * test: align vLLM limit expectation --------- Co-authored-by: yuhp <yu.haip@gmail.com> * docs: document vLLM model info enrichment * chore: bump version to 1.1.0 --------- Co-authored-by: Adele <virepri2k@gmail.com> Co-authored-by: yuhp <yu.haip@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent ee9da82 commit fdffd8b

13 files changed

Lines changed: 163 additions & 18 deletions

docs/configuration.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Each provider can configure discovery behavior through `provider.<name>.options.
3939
| `provider.<name>.options.modelsDiscovery.enabled` | `boolean` | Force enable or disable discovery for a single provider |
4040
| `provider.<name>.options.modelsDiscovery.endpoint` | `string` | Provider-specific models endpoint path. Defaults to `/v1/models` |
4141
| `provider.<name>.options.modelsDiscovery.modelInfoEndpoint` | `string` | Provider-specific model info endpoint path. Metadata enrichment is disabled when omitted |
42-
| `provider.<name>.options.modelsDiscovery.modelInfoFormat` | `string` | Model info response format. Currently supports `"litellm"` and `"models.dev"` |
42+
| `provider.<name>.options.modelsDiscovery.modelInfoFormat` | `string` | Model info response format. Currently supports `"litellm"`, `"models.dev"`, and `"vllm"` |
4343
| `provider.<name>.options.modelsDiscovery.filterNonChat` | `boolean` | When model info is available, skip models whose `model_info.mode` is not `chat`. Defaults to `true` |
4444
| `provider.<name>.options.modelsDiscovery.models.includeRegex` | `string[]` | Shortcut regex allow-list for discovered model ids only |
4545
| `provider.<name>.options.modelsDiscovery.models.excludeRegex` | `string[]` | Shortcut regex deny-list for discovered model ids only |
@@ -145,12 +145,13 @@ Community provider examples live in [`docs/config_example/`](config_example/).
145145

146146
The generic OpenAI-compatible `/v1/models` endpoint only guarantees a small model list shape. Extra metadata such as context limits, tool calling, reasoning, image input, or structured output is provider-specific, so metadata enrichment is opt-in.
147147

148-
The plugin currently supports two model info formats:
148+
The plugin currently supports three model info formats:
149149

150150
| Format | Source | Requires `modelInfoEndpoint` | Notes |
151151
|--------|--------|------------------------------|-------|
152152
| `"litellm"` | Provider-specific model info endpoint | Yes | Uses LiteLLM `/v1/model/info` responses |
153153
| `"models.dev"` | `https://models.dev/models.json` | No | Uses the public models.dev metadata index |
154+
| `"vllm"` | Fields in the provider's `/v1/models` response | No | Reads vLLM-style `max_model_len` when present |
154155

155156
### LiteLLM Model Info
156157

@@ -187,6 +188,34 @@ When model info is available, the plugin uses LiteLLM `model_info` fields to pop
187188
- `supports_*_reasoning_effort` and `supported_openai_params` create reasoning `variants`
188189
- By default, entries whose `model_info.mode` is not `chat` are skipped
189190

191+
### vLLM Model Info
192+
193+
Use `modelInfoFormat: "vllm"` for a vLLM-compatible provider whose `/v1/models` response includes a numeric `max_model_len` field for each model. This does not make another metadata request.
194+
195+
```json
196+
{
197+
"plugin": ["opencode-models-discovery"],
198+
"provider": {
199+
"local-vllm": {
200+
"npm": "@ai-sdk/openai-compatible",
201+
"name": "Local vLLM",
202+
"options": {
203+
"baseURL": "http://127.0.0.1:8000/v1",
204+
"modelsDiscovery": {
205+
"enabled": true,
206+
"modelInfoFormat": "vllm"
207+
}
208+
},
209+
"models": {}
210+
}
211+
}
212+
}
213+
```
214+
215+
For each discovered model with a positive numeric `max_model_len`, the plugin sets `limit.context` and `limit.output` to that value. `max_model_len` represents the total request sequence length shared by prompt and generated tokens; it is not used as an independent input limit.
216+
217+
`max_model_len` is not part of the standard OpenAI-compatible `/v1/models` response. If a vLLM deployment or proxy does not expose it, discovery still succeeds but no limit is added. This format does not infer reasoning, tool-calling, modalities, or other capabilities.
218+
190219
### models.dev Metadata
191220

192221
Use `modelInfoFormat: "models.dev"` to enrich discovered models from the public [models.dev](https://models.dev) metadata index.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "opencode-models-discovery",
4-
"version": "1.0.2",
4+
"version": "1.1.0",
55
"description": "OpenCode plugin for auto-discovery of OpenAI-compatible models with dynamic provider configuration",
66
"type": "module",
77
"main": "./src/index.ts",

src/plugin/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Supported plugin options under provider.<id>.options.modelsDiscovery:
116116
- smartModelName: use friendlier display names for discovered models
117117
- modelInfoFormat="models.dev": enrich from the public models.dev index without modelInfoEndpoint
118118
- modelInfoEndpoint plus modelInfoFormat="litellm": enrich from a LiteLLM-compatible model info endpoint
119+
- modelInfoFormat="vllm": for vLLM-compatible providers whose raw /v1/models entries include a positive numeric max_model_len; without another request, sets limit.context and limit.output for matching discovered models
119120
- filterNonChat: when LiteLLM model info is available, skip non-chat models by default
120121
121122
Recommended defaults:
@@ -135,6 +136,7 @@ Recommended defaults:
135136
- use smartModelName=true only when the user wants friendlier display names
136137
- use modelInfoFormat="models.dev" for models.dev metadata enrichment
137138
- use modelInfoEndpoint and modelInfoFormat="litellm" for LiteLLM-compatible model info endpoints
139+
- use modelInfoFormat="vllm" only when the provider's /v1/models response exposes max_model_len; it is not a standard OpenAI-compatible field, does not require modelInfoEndpoint, and does not infer other capabilities
138140
139141
Provider compatibility guidance:
140142
- Discovery works for @ai-sdk/openai-compatible providers by default.

src/plugin/enhance-config.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ToastNotifier } from '../ui/toast-notifier'
55
import { categorizeModel, formatModelName, extractModelOwner } from '../utils'
66
import { normalizeBaseURL, discoverModelsFromProvider, discoverModelInfoFromProvider, canDiscoverModels } from '../utils/openai-compatible-api'
77
import { createModelInfoEnricher, isSupportedModelInfoFormat, type ModelInfoEnricher } from '../utils/model-info'
8-
import { getDefaultDiscoveryConfigFromEnv, getProviderModelFieldFilters, getProviderModelRegexFilter, shouldDiscoverModel, shouldDiscoverModelByFields, shouldDiscoverProviderWithOverride } from '../types/plugin-config'
8+
import { getDefaultDiscoveryConfigFromEnv, getProviderModelFieldFilters, getProviderModelRegexFilter, shouldDiscoverModel, shouldDiscoverModelByFields, shouldDiscoverProviderWithOverride, ModelInfoFormat } from '../types/plugin-config'
99
import { fetchModelsDevData } from '../utils/models-dev-fetcher'
1010
import type { PluginLogger } from './logger'
1111
import type { PluginInput } from '@opencode-ai/plugin'
@@ -231,13 +231,15 @@ export async function enhanceConfig(
231231
provider: providerName,
232232
format: modelInfoFormat,
233233
})
234-
} else if (modelInfoFormat === 'models.dev') {
234+
} else if (modelInfoFormat === ModelInfoFormat.ModelsDev) {
235235
const modelsDevCache = await fetchModelsDevData()
236236
modelInfoEnricher = createModelInfoEnricher(modelInfoFormat, modelsDevCache, { filterNonChat })
237237
logger.info('Loaded models.dev data', {
238238
provider: providerName,
239239
count: modelsDevCache.size,
240240
})
241+
} else if (modelInfoFormat === ModelInfoFormat.VLLM) {
242+
modelInfoEnricher = createModelInfoEnricher(modelInfoFormat, null)
241243
} else if (typeof modelInfoEndpoint === 'string' && modelInfoEndpoint.length > 0 && modelInfoFormat) {
242244
const modelInfoDiscovery = await discoverModelInfoFromProvider(baseURL, apiKey, modelInfoEndpoint)
243245
if (modelInfoDiscovery.ok) {
@@ -299,7 +301,7 @@ export async function enhanceConfig(
299301
}
300302
}
301303

302-
modelInfoEnricher?.applyModelInfo(modelConfig, model.id)
304+
modelInfoEnricher?.applyModelInfo(modelConfig, model.id, model)
303305

304306
discoveredModels[modelKey] = modelConfig
305307
}

src/types/plugin-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ export type ModelFieldFilter = ModelFieldEqualsFilter | ModelFieldMatchFilter
2929

3030
export type CompiledModelFieldFilter = ModelFieldEqualsFilter | (Omit<ModelFieldMatchFilter, 'match'> & { match: RegExp })
3131

32-
export type ModelInfoFormat = 'litellm' | 'models.dev' | (string & {})
32+
export enum ModelInfoFormat {
33+
LiteLLM = 'litellm',
34+
ModelsDev = 'models.dev',
35+
VLLM = 'vllm',
36+
}
3337

3438
export interface ProviderDiscoveryConfig {
3539
enabled?: boolean

src/utils/model-info/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { createLiteLLMModelInfoEnricher } from './litellm'
22
import { createModelsDevModelInfoEnricher } from './models-dev'
3-
import type { ModelInfoFormat } from '../../types/plugin-config'
3+
import { createVLLMModelInfoEnricher } from './vllm'
4+
import { ModelInfoFormat } from '../../types/plugin-config'
45
import type { ModelInfoEnricher, ModelInfoEnricherOptions } from './types'
56

6-
type ModelInfoEnricherFactory = (data: unknown, options: ModelInfoEnricherOptions) => ModelInfoEnricher
7+
type ModelInfoEnricherFactory = (data: unknown, options?: ModelInfoEnricherOptions) => ModelInfoEnricher
78

8-
const MODEL_INFO_ENRICHERS: Record<string, ModelInfoEnricherFactory> = {
9-
litellm: createLiteLLMModelInfoEnricher,
10-
'models.dev': createModelsDevModelInfoEnricher,
9+
const MODEL_INFO_ENRICHERS: Partial<Record<ModelInfoFormat, ModelInfoEnricherFactory>> = {
10+
[ModelInfoFormat.LiteLLM]: createLiteLLMModelInfoEnricher,
11+
[ModelInfoFormat.ModelsDev]: createModelsDevModelInfoEnricher,
12+
[ModelInfoFormat.VLLM]: createVLLMModelInfoEnricher,
1113
}
1214

1315
export function createModelInfoEnricher(
1416
format: ModelInfoFormat,
1517
data: unknown,
16-
options: ModelInfoEnricherOptions
18+
options?: ModelInfoEnricherOptions
1719
): ModelInfoEnricher | undefined {
1820
return MODEL_INFO_ENRICHERS[format]?.(data, options)
1921
}

src/utils/model-info/litellm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,14 @@ function getModelInfo(modelInfoById: Map<string, LiteLLMModelInfoEntry>, modelId
100100

101101
export function createLiteLLMModelInfoEnricher(
102102
data: unknown,
103-
options: ModelInfoEnricherOptions
103+
options?: ModelInfoEnricherOptions
104104
): ModelInfoEnricher {
105105
const response = data as { data?: LiteLLMModelInfoEntry[] } | undefined
106106
const modelInfoById = buildModelInfoMap(Array.isArray(response?.data) ? response.data : [])
107107

108108
return {
109109
shouldSkipModel(modelId: string): boolean {
110-
if (!options.filterNonChat) return false
110+
if (!options?.filterNonChat) return false
111111
const mode = getModelInfo(modelInfoById, modelId)?.model_info?.mode
112112
return typeof mode === 'string' && mode.length > 0 && mode !== 'chat'
113113
},

src/utils/model-info/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export interface ModelInfoEnricher {
22
shouldSkipModel(modelId: string): boolean
33
getModelName?(modelId: string): string | undefined
4-
applyModelInfo(modelConfig: any, modelId: string): void
4+
applyModelInfo(modelConfig: any, modelId: string, rawModel?: Record<string, unknown>): void
55
}
66

77
export interface ModelInfoEnricherOptions {

src/utils/model-info/vllm.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { ModelInfoEnricher, ModelInfoEnricherOptions } from './types'
2+
3+
function hasUsableNumber(value: unknown): value is number {
4+
return typeof value === 'number' && Number.isFinite(value) && value > 0
5+
}
6+
7+
export function createVLLMModelInfoEnricher(_data: unknown, _options?: ModelInfoEnricherOptions): ModelInfoEnricher {
8+
return {
9+
shouldSkipModel(): boolean {
10+
return false
11+
},
12+
applyModelInfo(modelConfig: any, _modelId: string, rawModel?: Record<string, unknown>): void {
13+
const maxModelLen = rawModel?.max_model_len
14+
if (hasUsableNumber(maxModelLen)) {
15+
modelConfig.limit = {
16+
context: maxModelLen,
17+
output: maxModelLen,
18+
}
19+
}
20+
},
21+
}
22+
}

0 commit comments

Comments
 (0)