feat: Add provider categorization and improved UI for model selection

- Add comprehensive provider metadata system with categories (cloud/local/hybrid)
- Add pricing models (free/paid/freemium) and quality tiers
- Create new CategorizedProviderSelect component with visual organization
- Add icons and color-coded badges for better UX
- Include warnings for deprecated/experimental providers
- Support 50+ providers with detailed metadata

Fixes #8618
This commit is contained in:
Roo Code 2025-10-12 00:48:43 +00:00
parent 3a47c55a2e
commit bde80f807d
5 changed files with 1256 additions and 7 deletions

View file

@ -13,6 +13,8 @@ export * from "./mcp.js"
export * from "./message.js"
export * from "./mode.js"
export * from "./model.js"
export * from "./provider-metadata.js"
export * from "./provider-metadata-definitions.js"
export * from "./provider-settings.js"
export * from "./single-file-read-models.js"
export * from "./task.js"

View file

@ -0,0 +1,821 @@
import { ProviderMetadataMap } from "./provider-metadata.js"
/**
* Comprehensive provider metadata with categorization
*/
export const PROVIDER_METADATA: ProviderMetadataMap = {
// Premium Cloud Providers (Paid, High Quality)
"openai-native": {
id: "openai-native",
label: "OpenAI",
category: "cloud",
pricing: "pay-per-use",
quality: "premium",
authentication: "api-key",
recommended: true,
deprecated: false,
description: "Official OpenAI API with GPT-4, GPT-4 Turbo, and GPT-5 models",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
supportsPromptCaching: true,
},
performance: {
speed: "fast",
reliability: "high",
},
costInfo: {
pricingUrl: "https://openai.com/pricing",
},
tags: ["official", "gpt", "premium"],
},
anthropic: {
id: "anthropic",
label: "Anthropic",
category: "cloud",
pricing: "pay-per-use",
quality: "premium",
authentication: "api-key",
recommended: true,
deprecated: false,
description: "Claude models with strong reasoning and coding capabilities",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
supportsPromptCaching: true,
},
performance: {
speed: "fast",
reliability: "high",
contextWindow: 200000,
},
costInfo: {
pricingUrl: "https://www.anthropic.com/pricing",
},
tags: ["official", "claude", "premium"],
},
gemini: {
id: "gemini",
label: "Google Gemini",
category: "cloud",
pricing: "freemium",
quality: "premium",
authentication: "api-key",
recommended: true,
deprecated: false,
description: "Google's Gemini models with large context windows",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
contextWindow: 1000000,
},
costInfo: {
pricingUrl: "https://ai.google.dev/pricing",
freeTokensPerMonth: 60,
},
tags: ["official", "google", "large-context"],
},
// Enterprise Cloud Providers
bedrock: {
id: "bedrock",
label: "Amazon Bedrock",
category: "cloud",
pricing: "pay-per-use",
quality: "premium",
authentication: "cloud-account",
recommended: true,
deprecated: false,
description: "AWS managed service for various AI models",
setupDifficulty: "advanced",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
supportsPromptCaching: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["enterprise", "aws", "multi-model"],
},
vertex: {
id: "vertex",
label: "GCP Vertex AI",
category: "cloud",
pricing: "pay-per-use",
quality: "premium",
authentication: "cloud-account",
recommended: true,
deprecated: false,
description: "Google Cloud's enterprise AI platform",
setupDifficulty: "advanced",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["enterprise", "gcp", "multi-model"],
},
// Local Providers (Free, Self-hosted)
ollama: {
id: "ollama",
label: "Ollama",
category: "local",
pricing: "free",
quality: "standard",
authentication: "none",
recommended: true,
deprecated: false,
description: "Run AI models locally on your machine",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "high",
},
tags: ["local", "open-source", "privacy"],
},
lmstudio: {
id: "lmstudio",
label: "LM Studio",
category: "local",
pricing: "free",
quality: "standard",
authentication: "none",
recommended: true,
deprecated: false,
description: "User-friendly local model runner with GUI",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "high",
},
tags: ["local", "user-friendly", "privacy"],
},
// Router/Aggregator Services
openrouter: {
id: "openrouter",
label: "OpenRouter",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: true,
deprecated: false,
description: "Access multiple AI providers through one API",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
supportsPromptCaching: true,
},
performance: {
speed: "fast",
reliability: "high",
},
costInfo: {
pricingUrl: "https://openrouter.ai/pricing",
},
tags: ["router", "multi-provider", "flexible"],
},
litellm: {
id: "litellm",
label: "LiteLLM",
category: "hybrid",
pricing: "freemium",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Unified interface for multiple LLM providers",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["router", "proxy", "multi-provider"],
},
"vercel-ai-gateway": {
id: "vercel-ai-gateway",
label: "Vercel AI Gateway",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Vercel's unified AI gateway with caching",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["router", "vercel", "caching"],
},
// Alternative/Specialized Providers
deepseek: {
id: "deepseek",
label: "DeepSeek",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: true,
deprecated: false,
description: "Cost-effective Chinese AI provider with strong coding models",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["affordable", "coding", "chinese"],
},
groq: {
id: "groq",
label: "Groq",
category: "cloud",
pricing: "freemium",
quality: "standard",
authentication: "api-key",
recommended: true,
deprecated: false,
description: "Ultra-fast inference with specialized hardware",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["fast", "free-tier", "specialized"],
},
cerebras: {
id: "cerebras",
label: "Cerebras",
category: "cloud",
pricing: "freemium",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Fast inference with Cerebras hardware",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "fast",
reliability: "medium",
},
tags: ["fast", "specialized-hardware"],
},
mistral: {
id: "mistral",
label: "Mistral",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "European AI provider with efficient models",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["european", "efficient"],
},
xai: {
id: "xai",
label: "xAI (Grok)",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Grok models from xAI",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "medium",
},
tags: ["grok", "new"],
},
// Specialized/Niche Providers
"claude-code": {
id: "claude-code",
label: "Claude Code",
category: "cloud",
pricing: "subscription",
quality: "premium",
authentication: "oauth",
recommended: true,
deprecated: false,
description: "Claude optimized for coding tasks",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["claude", "coding", "specialized"],
},
"qwen-code": {
id: "qwen-code",
label: "Qwen Code",
category: "cloud",
pricing: "free",
quality: "standard",
authentication: "oauth",
recommended: false,
deprecated: false,
description: "Alibaba's Qwen model for coding",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: true,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["alibaba", "coding", "chinese"],
},
moonshot: {
id: "moonshot",
label: "Moonshot",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Chinese AI provider with large context models",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["chinese", "large-context"],
},
doubao: {
id: "doubao",
label: "Doubao",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "ByteDance's AI models",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["bytedance", "chinese"],
},
// Infrastructure/Development Providers
deepinfra: {
id: "deepinfra",
label: "DeepInfra",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Serverless inference for open-source models",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["open-source", "serverless"],
},
huggingface: {
id: "huggingface",
label: "Hugging Face",
category: "cloud",
pricing: "freemium",
quality: "experimental",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Access thousands of open-source models",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["open-source", "community", "experimental"],
},
sambanova: {
id: "sambanova",
label: "SambaNova",
category: "cloud",
pricing: "freemium",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Fast inference with SambaNova hardware",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "fast",
reliability: "medium",
},
tags: ["specialized-hardware", "free-tier"],
},
fireworks: {
id: "fireworks",
label: "Fireworks AI",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Fast and affordable AI inference",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["fast", "affordable"],
},
// Custom/Compatible Providers
openai: {
id: "openai",
label: "OpenAI Compatible",
category: "hybrid",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Connect to any OpenAI-compatible API endpoint",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "medium",
},
warning: "Compatibility varies by provider",
tags: ["flexible", "custom", "compatible"],
},
// VSCode Integration
"vscode-lm": {
id: "vscode-lm",
label: "VS Code LM API",
category: "local",
pricing: "free",
quality: "standard",
authentication: "none",
recommended: false,
deprecated: false,
description: "Use models provided by VS Code extensions",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "high",
},
tags: ["vscode", "integrated", "copilot"],
},
// Lesser-known/Experimental Providers
glama: {
id: "glama",
label: "Glama",
category: "cloud",
pricing: "pay-per-use",
quality: "experimental",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Multi-model AI gateway",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["gateway", "experimental"],
},
unbound: {
id: "unbound",
label: "Unbound",
category: "cloud",
pricing: "pay-per-use",
quality: "experimental",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Experimental AI provider",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "low",
},
tags: ["experimental"],
},
requesty: {
id: "requesty",
label: "Requesty",
category: "cloud",
pricing: "pay-per-use",
quality: "experimental",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "API gateway service",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "low",
},
tags: ["gateway", "experimental"],
},
chutes: {
id: "chutes",
label: "Chutes AI",
category: "cloud",
pricing: "pay-per-use",
quality: "experimental",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Experimental AI provider",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "low",
},
tags: ["experimental"],
},
featherless: {
id: "featherless",
label: "Featherless AI",
category: "cloud",
pricing: "pay-per-use",
quality: "experimental",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Serverless AI inference",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "low",
},
tags: ["serverless", "experimental"],
},
"io-intelligence": {
id: "io-intelligence",
label: "IO Intelligence",
category: "cloud",
pricing: "pay-per-use",
quality: "experimental",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Experimental AI provider",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "low",
},
tags: ["experimental"],
},
zai: {
id: "zai",
label: "Z AI",
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
description: "Zhipu AI models",
setupDifficulty: "moderate",
features: {
supportsStreaming: true,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "medium",
reliability: "medium",
},
tags: ["chinese", "zhipu"],
},
// Special/Internal Providers
roo: {
id: "roo",
label: "Roo Code Cloud",
category: "cloud",
pricing: "subscription",
quality: "premium",
authentication: "cloud-account",
recommended: true,
deprecated: false,
description: "Roo Code's managed cloud service",
setupDifficulty: "easy",
features: {
supportsStreaming: true,
supportsImages: true,
supportsTools: true,
supportsPromptCaching: true,
},
performance: {
speed: "fast",
reliability: "high",
},
tags: ["official", "managed", "integrated"],
},
"human-relay": {
id: "human-relay",
label: "Human Relay",
category: "local",
pricing: "free",
quality: "experimental",
authentication: "none",
recommended: false,
deprecated: false,
description: "Manual responses for testing",
setupDifficulty: "easy",
features: {
supportsStreaming: false,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "slow",
reliability: "low",
},
warning: "For testing only - requires manual intervention",
tags: ["testing", "manual", "development"],
},
"fake-ai": {
id: "fake-ai",
label: "Fake AI",
category: "local",
pricing: "free",
quality: "experimental",
authentication: "none",
recommended: false,
deprecated: false,
description: "Mock AI for testing",
setupDifficulty: "easy",
features: {
supportsStreaming: false,
supportsImages: false,
supportsTools: false,
},
performance: {
speed: "fast",
reliability: "high",
},
warning: "For testing only - returns mock responses",
tags: ["testing", "mock", "development"],
},
}

View file

@ -0,0 +1,77 @@
/**
* Provider metadata for enhanced categorization and user guidance
*/
export type ProviderCategory = "cloud" | "local" | "hybrid"
export type PricingModel = "free" | "paid" | "freemium" | "pay-per-use" | "subscription"
export type QualityTier = "premium" | "standard" | "experimental" | "deprecated"
export type AuthenticationMethod = "api-key" | "oauth" | "none" | "cloud-account"
export interface ProviderMetadata {
/** Unique identifier for the provider */
id: string
/** Display name for the provider */
label: string
/** Category of the provider */
category: ProviderCategory
/** Pricing model */
pricing: PricingModel
/** Quality tier indicator */
quality: QualityTier
/** Authentication method required */
authentication: AuthenticationMethod
/** Whether this provider is recommended */
recommended: boolean
/** Whether this provider should be avoided */
deprecated: boolean
/** Short description of the provider */
description?: string
/** Setup difficulty level */
setupDifficulty?: "easy" | "moderate" | "advanced"
/** Link to documentation */
documentationUrl?: string
/** Specific features or capabilities */
features?: {
supportsStreaming?: boolean
supportsImages?: boolean
supportsTools?: boolean
supportsPromptCaching?: boolean
supportsFinetuning?: boolean
}
/** Performance characteristics */
performance?: {
speed: "fast" | "medium" | "slow"
reliability: "high" | "medium" | "low"
contextWindow?: number
}
/** Cost information for paid providers */
costInfo?: {
pricingUrl?: string
estimatedCostPer1kTokens?: number
freeTokensPerMonth?: number
}
/** Warning message if any */
warning?: string
/** Tags for additional categorization */
tags?: string[]
}
export type ProviderMetadataMap = Record<string, ProviderMetadata>

View file

@ -56,11 +56,11 @@ import {
SelectValue,
SelectContent,
SelectItem,
SearchableSelect,
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
} from "@src/components/ui"
import { CategorizedProviderSelect } from "./CategorizedProviderSelect"
import {
Anthropic,
@ -443,15 +443,11 @@ const ApiOptions = ({
</div>
)}
</div>
<SearchableSelect
<CategorizedProviderSelect
value={selectedProvider}
onValueChange={(value) => onProviderChange(value as ProviderName)}
options={providerOptions}
availableProviders={providerOptions}
placeholder={t("settings:common.select")}
searchPlaceholder={t("settings:providers.searchProviderPlaceholder")}
emptyMessage={t("settings:providers.noProviderMatchFound")}
className="w-full"
data-testid="provider-select"
/>
</div>

View file

@ -0,0 +1,353 @@
import React, { useMemo, useState } from "react"
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { ProviderMetadata, PROVIDER_METADATA, ProviderCategory, PricingModel } from "@roo-code/types"
import { cn } from "@src/lib/utils"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
interface CategorizedProviderSelectProps {
value: string
onValueChange: (value: string) => void
availableProviders: Array<{ value: string; label: string }>
placeholder?: string
}
// Helper function to get badge styles based on pricing
const getPricingBadgeStyles = (pricing: PricingModel) => {
switch (pricing) {
case "free":
return "bg-green-500/20 text-green-700 dark:text-green-400"
case "freemium":
return "bg-blue-500/20 text-blue-700 dark:text-blue-400"
case "pay-per-use":
return "bg-orange-500/20 text-orange-700 dark:text-orange-400"
case "subscription":
return "bg-purple-500/20 text-purple-700 dark:text-purple-400"
default:
return "bg-gray-500/20 text-gray-700 dark:text-gray-400"
}
}
// Helper function to get badge styles based on quality
const getQualityBadgeStyles = (quality: string) => {
switch (quality) {
case "premium":
return "bg-yellow-500/20 text-yellow-700 dark:text-yellow-400"
case "standard":
return "bg-gray-500/20 text-gray-700 dark:text-gray-400"
case "experimental":
return "bg-red-500/20 text-red-700 dark:text-red-400"
case "deprecated":
return "bg-red-800/20 text-red-900 dark:text-red-300"
default:
return "bg-gray-500/20 text-gray-700 dark:text-gray-400"
}
}
// Helper function to get category icon
const getCategoryIcon = (category: ProviderCategory) => {
switch (category) {
case "cloud":
return "☁️"
case "local":
return "💻"
case "hybrid":
return "🔄"
default:
return "📦"
}
}
export const CategorizedProviderSelect: React.FC<CategorizedProviderSelectProps> = ({
value,
onValueChange,
availableProviders,
placeholder = "Select a provider",
}) => {
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set(["recommended", "cloud", "local"]),
)
// Group providers by category
const categorizedProviders = useMemo(() => {
const groups: Record<string, Array<{ provider: ProviderMetadata; value: string }>> = {
recommended: [],
cloud: [],
local: [],
hybrid: [],
experimental: [],
deprecated: [],
}
availableProviders.forEach(({ value: providerId }) => {
const metadata = PROVIDER_METADATA[providerId]
if (metadata) {
const provider = { provider: metadata, value: providerId }
// Add to recommended section if applicable
if (metadata.recommended) {
groups.recommended.push(provider)
}
// Add to quality-based sections
if (metadata.deprecated || metadata.quality === "deprecated") {
groups.deprecated.push(provider)
} else if (metadata.quality === "experimental") {
groups.experimental.push(provider)
} else {
// Add to category-based section
groups[metadata.category]?.push(provider)
}
} else {
// Fallback for providers without metadata
groups.cloud.push({
provider: {
id: providerId,
label: availableProviders.find((p) => p.value === providerId)?.label || providerId,
category: "cloud",
pricing: "pay-per-use",
quality: "standard",
authentication: "api-key",
recommended: false,
deprecated: false,
},
value: providerId,
})
}
})
// Sort providers within each group
Object.keys(groups).forEach((key) => {
groups[key].sort((a, b) => a.provider.label.localeCompare(b.provider.label))
})
// Remove empty groups
return Object.entries(groups).filter(([_, providers]) => providers.length > 0)
}, [availableProviders])
const toggleCategory = (category: string) => {
setExpandedCategories((prev) => {
const newSet = new Set(prev)
if (newSet.has(category)) {
newSet.delete(category)
} else {
newSet.add(category)
}
return newSet
})
}
const getCategoryLabel = (category: string) => {
switch (category) {
case "recommended":
return "⭐ Recommended"
case "cloud":
return "☁️ Cloud Providers"
case "local":
return "💻 Local Providers"
case "hybrid":
return "🔄 Hybrid Providers"
case "experimental":
return "🧪 Experimental"
case "deprecated":
return "⚠️ Deprecated"
default:
return category
}
}
const selectedMetadata = PROVIDER_METADATA[value]
const selectedLabel = selectedMetadata?.label || availableProviders.find((p) => p.value === value)?.label || value
return (
<div className="relative">
<Select value={value} onValueChange={onValueChange}>
<SelectTrigger className="w-full" data-testid="categorized-provider-select">
<SelectValue placeholder={placeholder}>
<div className="flex items-center gap-2">
{selectedMetadata && (
<span className="text-sm">{getCategoryIcon(selectedMetadata.category)}</span>
)}
<span>{selectedLabel}</span>
{selectedMetadata && (
<div className="flex gap-1 ml-auto">
<span
className={cn(
"text-xs px-1.5 py-0.5 rounded",
getPricingBadgeStyles(selectedMetadata.pricing),
)}>
{selectedMetadata.pricing}
</span>
{selectedMetadata.quality === "premium" && (
<span
className={cn(
"text-xs px-1.5 py-0.5 rounded",
getQualityBadgeStyles(selectedMetadata.quality),
)}>
Premium
</span>
)}
</div>
)}
</div>
</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-[400px] overflow-y-auto">
{categorizedProviders.map(([category, providers]) => (
<div key={category} className="mb-1">
<div
className="flex items-center gap-1 px-2 py-1.5 cursor-pointer hover:bg-vscode-list-hoverBackground text-sm font-medium"
onClick={() => toggleCategory(category)}>
{expandedCategories.has(category) ? (
<ChevronDownIcon className="w-3 h-3" />
) : (
<ChevronRightIcon className="w-3 h-3" />
)}
<span>{getCategoryLabel(category)}</span>
<span className="text-xs text-vscode-descriptionForeground ml-auto">
({providers.length})
</span>
</div>
{expandedCategories.has(category) && (
<div className="ml-2">
{providers.map(({ provider, value: providerId }) => (
<SelectItem key={providerId} value={providerId} className="pl-4 pr-2 py-2">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<span className="text-sm">
{getCategoryIcon(provider.category)}
</span>
<div className="flex flex-col">
<span className="text-sm">{provider.label}</span>
{provider.description && (
<span className="text-xs text-vscode-descriptionForeground">
{provider.description}
</span>
)}
</div>
</div>
<div className="flex gap-1 ml-4">
<span
className={cn(
"text-xs px-1.5 py-0.5 rounded",
getPricingBadgeStyles(provider.pricing),
)}>
{provider.pricing}
</span>
{provider.quality !== "standard" && (
<span
className={cn(
"text-xs px-1.5 py-0.5 rounded",
getQualityBadgeStyles(provider.quality),
)}>
{provider.quality}
</span>
)}
{provider.setupDifficulty === "advanced" && (
<span className="text-xs px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-700 dark:text-yellow-400">
Advanced
</span>
)}
</div>
</div>
</SelectItem>
))}
</div>
)}
</div>
))}
</SelectContent>
</Select>
{/* Show metadata info for selected provider */}
{selectedMetadata && (
<div className="mt-2 p-2 rounded bg-vscode-editor-background border border-vscode-panel-border">
<div className="flex items-start justify-between">
<div className="flex-1">
{selectedMetadata.description && (
<p className="text-sm text-vscode-descriptionForeground mb-2">
{selectedMetadata.description}
</p>
)}
{selectedMetadata.warning && (
<p className="text-sm text-yellow-600 dark:text-yellow-500 mb-2">
{selectedMetadata.warning}
</p>
)}
<div className="flex flex-wrap gap-2">
<span
className={cn(
"text-xs px-2 py-1 rounded",
getPricingBadgeStyles(selectedMetadata.pricing),
)}>
{selectedMetadata.pricing === "free"
? "Free"
: selectedMetadata.pricing === "freemium"
? "Free tier available"
: selectedMetadata.pricing === "pay-per-use"
? "Pay per use"
: selectedMetadata.pricing === "subscription"
? "Subscription"
: selectedMetadata.pricing}
</span>
{selectedMetadata.quality !== "standard" && (
<span
className={cn(
"text-xs px-2 py-1 rounded",
getQualityBadgeStyles(selectedMetadata.quality),
)}>
{selectedMetadata.quality === "premium"
? "Premium Quality"
: selectedMetadata.quality === "experimental"
? "Experimental"
: selectedMetadata.quality === "deprecated"
? "Deprecated"
: selectedMetadata.quality}
</span>
)}
{selectedMetadata.setupDifficulty && (
<span className="text-xs px-2 py-1 rounded bg-gray-500/20 text-gray-700 dark:text-gray-400">
Setup: {selectedMetadata.setupDifficulty}
</span>
)}
{selectedMetadata.performance && (
<>
<span className="text-xs px-2 py-1 rounded bg-blue-500/20 text-blue-700 dark:text-blue-400">
Speed: {selectedMetadata.performance.speed}
</span>
<span className="text-xs px-2 py-1 rounded bg-purple-500/20 text-purple-700 dark:text-purple-400">
Reliability: {selectedMetadata.performance.reliability}
</span>
</>
)}
</div>
{selectedMetadata.features && (
<div className="mt-2 flex flex-wrap gap-1">
{selectedMetadata.features.supportsStreaming && (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-700 dark:text-green-400">
Streaming
</span>
)}
{selectedMetadata.features.supportsImages && (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-700 dark:text-green-400">
Images
</span>
)}
{selectedMetadata.features.supportsTools && (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-700 dark:text-green-400">
Tools
</span>
)}
{selectedMetadata.features.supportsPromptCaching && (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-700 dark:text-green-400">
Caching
</span>
)}
</div>
)}
</div>
</div>
</div>
)}
</div>
)
}