mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-19 03:26:28 +00:00
* feat: integrate models.dev for dynamic LLM provider/model data (#TKT-657) Replace hardcoded model lists with data sourced from models.dev so new providers and models appear automatically when the community adds them. - Add build script (scripts/generate-models.ts) that fetches models.dev/api.json and outputs a compact JSON with 10 providers and 520 models - Replace hardcoded MODELS_DATA (50 models) with dynamic models.dev lookups - Add searchable model combobox (Popover + Command) replacing plain Select dropdown - Enrich provider templates with models.dev metadata (context window, image support) - Keep chatgpt-pro, qwen-code, browseros, openai-compatible as hardcoded providers * fix: address review — remove ollama-cloud mapping, fix default models, remove dead code - Remove ollama from PROVIDER_MAP (ollama-cloud has cloud models, not local) - Add ollama to CUSTOM_PROVIDER_MODELS with empty list (users type custom IDs) - Update defaultModelIds to ones that exist in models.dev data: openrouter → anthropic/claude-sonnet-4.5 lmstudio → openai/gpt-oss-20b bedrock → anthropic.claude-sonnet-4-6 - Remove dead isCustomModel export - Regenerate models-dev-data.json (9 providers, 486 models) * fix: model suggestion list focus/dismiss behavior - List only opens when input is focused or user types - Clicking a model selects it and closes the list - Clicking outside (blur) dismisses the list - onMouseDown preventDefault on list items prevents blur race condition * refactor: extract ModelPickerList component with proper open/close UX - Collapsed state: Select-like trigger showing selected model + chevron - Expanded state: search input + scrollable filtered list, inline - Click outside or Escape to close, Enter to submit custom model - Extracted as separate component (reduces dialog nesting, testable) - No more setTimeout hacks for blur handling * chore: remove plan doc from repo
146 lines
3.5 KiB
TypeScript
146 lines
3.5 KiB
TypeScript
/**
|
|
* Fetches models.dev/api.json and generates a compact models data file
|
|
* for BrowserOS. Run: bun scripts/generate-models.ts
|
|
*/
|
|
|
|
const API_URL = 'https://models.dev/api.json'
|
|
const OUTPUT_PATH = new URL(
|
|
'../apps/agent/lib/llm-providers/models-dev-data.json',
|
|
import.meta.url,
|
|
).pathname
|
|
|
|
interface ModelsDevModel {
|
|
id: string
|
|
name: string
|
|
family?: string
|
|
attachment: boolean
|
|
reasoning: boolean
|
|
tool_call: boolean
|
|
structured_output?: boolean
|
|
modalities: { input: string[]; output: string[] }
|
|
cost?: {
|
|
input: number
|
|
output: number
|
|
cache_read?: number
|
|
cache_write?: number
|
|
}
|
|
limit: { context: number; output: number; input?: number }
|
|
status?: string
|
|
release_date: string
|
|
last_updated: string
|
|
}
|
|
|
|
interface ModelsDevProvider {
|
|
id: string
|
|
name: string
|
|
npm: string
|
|
api?: string
|
|
doc: string
|
|
env: string[]
|
|
models: Record<string, ModelsDevModel>
|
|
}
|
|
|
|
interface OutputModel {
|
|
id: string
|
|
name: string
|
|
contextWindow: number
|
|
maxOutput: number
|
|
supportsImages: boolean
|
|
supportsReasoning: boolean
|
|
supportsToolCall: boolean
|
|
inputCost?: number
|
|
outputCost?: number
|
|
}
|
|
|
|
interface OutputProvider {
|
|
name: string
|
|
api?: string
|
|
doc: string
|
|
models: OutputModel[]
|
|
}
|
|
|
|
// models.dev ID → BrowserOS provider ID
|
|
const PROVIDER_MAP: Record<string, string> = {
|
|
anthropic: 'anthropic',
|
|
openai: 'openai',
|
|
google: 'google',
|
|
openrouter: 'openrouter',
|
|
azure: 'azure',
|
|
'amazon-bedrock': 'bedrock',
|
|
lmstudio: 'lmstudio',
|
|
moonshotai: 'moonshot',
|
|
'github-copilot': 'github-copilot',
|
|
}
|
|
|
|
function transformModel(model: ModelsDevModel): OutputModel | null {
|
|
if (model.status === 'deprecated') return null
|
|
|
|
const supportsImages =
|
|
model.attachment || model.modalities.input.includes('image')
|
|
|
|
return {
|
|
id: model.id,
|
|
name: model.name,
|
|
contextWindow: model.limit.context,
|
|
maxOutput: model.limit.output,
|
|
supportsImages,
|
|
supportsReasoning: model.reasoning,
|
|
supportsToolCall: model.tool_call,
|
|
...(model.cost && {
|
|
inputCost: model.cost.input,
|
|
outputCost: model.cost.output,
|
|
}),
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log(`Fetching ${API_URL}...`)
|
|
const response = await fetch(API_URL)
|
|
if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`)
|
|
|
|
const data: Record<string, ModelsDevProvider> = await response.json()
|
|
console.log(`Fetched ${Object.keys(data).length} providers`)
|
|
|
|
const output: Record<string, OutputProvider> = {}
|
|
|
|
for (const [modelsDevId, browserosId] of Object.entries(PROVIDER_MAP)) {
|
|
const provider = data[modelsDevId]
|
|
if (!provider) {
|
|
console.warn(`Provider not found in models.dev: ${modelsDevId}`)
|
|
continue
|
|
}
|
|
|
|
const models = Object.values(provider.models)
|
|
.map(transformModel)
|
|
.filter((m): m is OutputModel => m !== null)
|
|
.sort((a, b) => {
|
|
const dateA = provider.models[a.id]?.last_updated ?? ''
|
|
const dateB = provider.models[b.id]?.last_updated ?? ''
|
|
return dateB.localeCompare(dateA)
|
|
})
|
|
|
|
output[browserosId] = {
|
|
name: provider.name,
|
|
...(provider.api && { api: provider.api }),
|
|
doc: provider.doc,
|
|
models,
|
|
}
|
|
}
|
|
|
|
const totalModels = Object.values(output).reduce(
|
|
(sum, p) => sum + p.models.length,
|
|
0,
|
|
)
|
|
console.log(
|
|
`Generated ${Object.keys(output).length} providers with ${totalModels} models`,
|
|
)
|
|
|
|
await Bun.write(OUTPUT_PATH, JSON.stringify(output, null, 2))
|
|
console.log(`Written to ${OUTPUT_PATH}`)
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|