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
1,418 changes: 478 additions & 940 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/plugin/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Supported plugin options under provider.<id>.options.modelsDiscovery:
- smartModelName: use friendlier display names for discovered models
- modelInfoFormat="models.dev": enrich from the public models.dev index without modelInfoEndpoint
- modelInfoEndpoint plus modelInfoFormat="litellm": enrich from a LiteLLM-compatible model info endpoint
- modelInfoFormat="vllm": enrich from vLLM-compatible providers by reading max_model_len from the raw model listing response
- filterNonChat: when LiteLLM model info is available, skip non-chat models by default

Recommended defaults:
Expand All @@ -135,6 +136,7 @@ Recommended defaults:
- use smartModelName=true only when the user wants friendlier display names
- use modelInfoFormat="models.dev" for models.dev metadata enrichment
- use modelInfoEndpoint and modelInfoFormat="litellm" for LiteLLM-compatible model info endpoints
- use modelInfoFormat="vllm" for vLLM-compatible providers; no modelInfoEndpoint needed

Provider compatibility guidance:
- Discovery works for @ai-sdk/openai-compatible providers by default.
Expand Down
8 changes: 5 additions & 3 deletions src/plugin/enhance-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ToastNotifier } from '../ui/toast-notifier'
import { categorizeModel, formatModelName, extractModelOwner } from '../utils'
import { normalizeBaseURL, discoverModelsFromProvider, discoverModelInfoFromProvider, canDiscoverModels } from '../utils/openai-compatible-api'
import { createModelInfoEnricher, isSupportedModelInfoFormat, type ModelInfoEnricher } from '../utils/model-info'
import { getDefaultDiscoveryConfigFromEnv, getProviderModelFieldFilters, getProviderModelRegexFilter, shouldDiscoverModel, shouldDiscoverModelByFields, shouldDiscoverProviderWithOverride } from '../types/plugin-config'
import { getDefaultDiscoveryConfigFromEnv, getProviderModelFieldFilters, getProviderModelRegexFilter, shouldDiscoverModel, shouldDiscoverModelByFields, shouldDiscoverProviderWithOverride, ModelInfoFormat } from '../types/plugin-config'
import { fetchModelsDevData } from '../utils/models-dev-fetcher'
import type { PluginLogger } from './logger'
import type { PluginInput } from '@opencode-ai/plugin'
Expand Down Expand Up @@ -231,13 +231,15 @@ export async function enhanceConfig(
provider: providerName,
format: modelInfoFormat,
})
} else if (modelInfoFormat === 'models.dev') {
} else if (modelInfoFormat === ModelInfoFormat.ModelsDev) {
const modelsDevCache = await fetchModelsDevData()
modelInfoEnricher = createModelInfoEnricher(modelInfoFormat, modelsDevCache, { filterNonChat })
logger.info('Loaded models.dev data', {
provider: providerName,
count: modelsDevCache.size,
})
} else if (modelInfoFormat === ModelInfoFormat.VLLM) {
modelInfoEnricher = createModelInfoEnricher(modelInfoFormat, null)
} else if (typeof modelInfoEndpoint === 'string' && modelInfoEndpoint.length > 0 && modelInfoFormat) {
const modelInfoDiscovery = await discoverModelInfoFromProvider(baseURL, apiKey, modelInfoEndpoint)
if (modelInfoDiscovery.ok) {
Expand Down Expand Up @@ -299,7 +301,7 @@ export async function enhanceConfig(
}
}

modelInfoEnricher?.applyModelInfo(modelConfig, model.id)
modelInfoEnricher?.applyModelInfo(modelConfig, model.id, model)

discoveredModels[modelKey] = modelConfig
}
Expand Down
6 changes: 5 additions & 1 deletion src/types/plugin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export type ModelFieldFilter = ModelFieldEqualsFilter | ModelFieldMatchFilter

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

export type ModelInfoFormat = 'litellm' | 'models.dev' | (string & {})
export enum ModelInfoFormat {
LiteLLM = 'litellm',
ModelsDev = 'models.dev',
VLLM = 'vllm',
}

export interface ProviderDiscoveryConfig {
enabled?: boolean
Expand Down
14 changes: 8 additions & 6 deletions src/utils/model-info/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { createLiteLLMModelInfoEnricher } from './litellm'
import { createModelsDevModelInfoEnricher } from './models-dev'
import type { ModelInfoFormat } from '../../types/plugin-config'
import { createVLLMModelInfoEnricher } from './vllm'
import { ModelInfoFormat } from '../../types/plugin-config'
import type { ModelInfoEnricher, ModelInfoEnricherOptions } from './types'

type ModelInfoEnricherFactory = (data: unknown, options: ModelInfoEnricherOptions) => ModelInfoEnricher
type ModelInfoEnricherFactory = (data: unknown, options?: ModelInfoEnricherOptions) => ModelInfoEnricher

const MODEL_INFO_ENRICHERS: Record<string, ModelInfoEnricherFactory> = {
litellm: createLiteLLMModelInfoEnricher,
'models.dev': createModelsDevModelInfoEnricher,
const MODEL_INFO_ENRICHERS: Partial<Record<ModelInfoFormat, ModelInfoEnricherFactory>> = {
[ModelInfoFormat.LiteLLM]: createLiteLLMModelInfoEnricher,
[ModelInfoFormat.ModelsDev]: createModelsDevModelInfoEnricher,
[ModelInfoFormat.VLLM]: createVLLMModelInfoEnricher,
}

export function createModelInfoEnricher(
format: ModelInfoFormat,
data: unknown,
options: ModelInfoEnricherOptions
options?: ModelInfoEnricherOptions
): ModelInfoEnricher | undefined {
return MODEL_INFO_ENRICHERS[format]?.(data, options)
}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/model-info/litellm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ function getModelInfo(modelInfoById: Map<string, LiteLLMModelInfoEntry>, modelId

export function createLiteLLMModelInfoEnricher(
data: unknown,
options: ModelInfoEnricherOptions
options?: ModelInfoEnricherOptions
): ModelInfoEnricher {
const response = data as { data?: LiteLLMModelInfoEntry[] } | undefined
const modelInfoById = buildModelInfoMap(Array.isArray(response?.data) ? response.data : [])

return {
shouldSkipModel(modelId: string): boolean {
if (!options.filterNonChat) return false
if (!options?.filterNonChat) return false
const mode = getModelInfo(modelInfoById, modelId)?.model_info?.mode
return typeof mode === 'string' && mode.length > 0 && mode !== 'chat'
},
Expand Down
2 changes: 1 addition & 1 deletion src/utils/model-info/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export interface ModelInfoEnricher {
shouldSkipModel(modelId: string): boolean
getModelName?(modelId: string): string | undefined
applyModelInfo(modelConfig: any, modelId: string): void
applyModelInfo(modelConfig: any, modelId: string, rawModel?: Record<string, unknown>): void
}

export interface ModelInfoEnricherOptions {
Expand Down
23 changes: 23 additions & 0 deletions src/utils/model-info/vllm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ModelInfoEnricher, ModelInfoEnricherOptions } from './types'

function hasUsableNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value > 0
}

export function createVLLMModelInfoEnricher(_data: unknown, _options?: ModelInfoEnricherOptions): ModelInfoEnricher {
return {
shouldSkipModel(): boolean {
return false
},
applyModelInfo(modelConfig: any, _modelId: string, rawModel?: Record<string, unknown>): void {
const maxModelLen = rawModel?.max_model_len
if (hasUsableNumber(maxModelLen)) {
modelConfig.limit = {
context: maxModelLen,
input: maxModelLen,
output: maxModelLen,
}
}
},
}
}
39 changes: 39 additions & 0 deletions test/plugin-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect } from 'vitest'
import { isSupportedModelInfoFormat } from '../src/utils/model-info'

describe('JSON config struct parsing', () => {
function parse(json: string): Record<string, unknown> {
return JSON.parse(json)
}

it.each([
{ json: '{"modelInfoFormat":"litellm"}', expected: true },
{ json: '{"modelInfoFormat":"models.dev"}', expected: true },
{ json: '{"modelInfoFormat":"vllm"}', expected: true },
{ json: '{"modelInfoFormat":"bogus"}', expected: false },
])('handles modelInfoFormat=$json', ({ json, expected }) => {
const config = parse(json)
expect(isSupportedModelInfoFormat(config.modelInfoFormat)).toBe(expected)
})

it('handles absence of modelInfoFormat', () => {
const config = parse('{"enabled":true}')
expect(config.modelInfoFormat).toBeUndefined()
})

it('handles a full provider discovery config from JSON', () => {
const json = `{
"enabled": true,
"endpoint": "/v1/models",
"modelInfoEndpoint": "/v1/model/info",
"modelInfoFormat": "litellm",
"filterNonChat": true,
"models": {
"includeBy": [{ "field": "id", "match": "^chat-" }]
}
}`
const config = parse(json)
expect(config.modelInfoFormat).toBe('litellm')
expect(isSupportedModelInfoFormat(config.modelInfoFormat)).toBe(true)
})
})
44 changes: 44 additions & 0 deletions test/vllm-model-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest'
import { ModelInfoFormat } from '../src/types/plugin-config'
import { createModelInfoEnricher } from '../src/utils/model-info'

describe('vLLM model info enricher', () => {
it('extracts max_model_len from raw model', () => {
const enricher = createModelInfoEnricher(ModelInfoFormat.VLLM, null)
expect(enricher).toBeDefined()

const modelConfig: any = { id: 'test-model' }
const rawModel: Record<string, unknown> = {
id: 'test-model',
object: 'model',
created: 1,
owned_by: 'vllm',
max_model_len: 8192,
}

enricher!.applyModelInfo(modelConfig, 'test-model', rawModel)
expect(modelConfig.limit).toEqual({
context: 8192,
input: 8192,
output: 8192,
})
})

it('does not set limit when max_model_len is missing', () => {
const enricher = createModelInfoEnricher(ModelInfoFormat.VLLM, null)
expect(enricher).toBeDefined()

const modelConfig: any = { id: 'test-model' }
enricher!.applyModelInfo(modelConfig, 'test-model', { id: 'test-model', object: 'model', created: 1, owned_by: 'llama.cpp' })
expect(modelConfig.limit).toBeUndefined()
})

it('does not set limit for non-positive max_model_len values', () => {
const enricher = createModelInfoEnricher(ModelInfoFormat.VLLM, null)
expect(enricher).toBeDefined()

const modelConfig: any = { id: 'test-model' }
enricher!.applyModelInfo(modelConfig, 'test-model', { id: 'test-model', object: 'model', created: 1, owned_by: 'vllm', max_model_len: 0 })
expect(modelConfig.limit).toBeUndefined()
})
})