diff --git a/app/components/@settings/tabs/providers/components/ModelInput.tsx b/app/components/@settings/tabs/providers/components/ModelInput.tsx new file mode 100644 index 0000000000..95afc23d87 --- /dev/null +++ b/app/components/@settings/tabs/providers/components/ModelInput.tsx @@ -0,0 +1,136 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { Input } from '~/components/ui/Input'; +import { Label } from '~/components/ui/Label'; +import { CheckCircle, AlertCircle } from 'lucide-react'; + +interface ModelInputProps { + provider: string; + value: string; + onChange: (value: string) => void; + suggestedModels?: string[]; + validateModel?: (model: string) => boolean; + placeholder?: string; + label?: string; + helpText?: string; +} + +export function ModelInput({ + provider, + value, + onChange, + suggestedModels = [], + validateModel, + placeholder = 'Enter model name', + label = 'Custom Model', + helpText, +}: ModelInputProps) { + const [inputValue, setInputValue] = useState(value); + const [isValid, setIsValid] = useState(null); + const [showSuggestions, setShowSuggestions] = useState(false); + const [filteredSuggestions, setFilteredSuggestions] = useState([]); + + useEffect(() => { + setInputValue(value); + }, [value]); + + useEffect(() => { + if (inputValue && suggestedModels.length > 0) { + const filtered = suggestedModels.filter((model) => model.toLowerCase().includes(inputValue.toLowerCase())); + setFilteredSuggestions(filtered); + } else { + setFilteredSuggestions(suggestedModels); + } + }, [inputValue, suggestedModels]); + + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInputValue(newValue); + setShowSuggestions(true); + + if (newValue && validateModel) { + setIsValid(validateModel(newValue)); + } else { + setIsValid(null); + } + }, + [validateModel], + ); + + const handleBlur = useCallback(() => { + setTimeout(() => setShowSuggestions(false), 200); + + if (inputValue !== value) { + onChange(inputValue); + } + }, [inputValue, value, onChange]); + + const handleSuggestionClick = useCallback( + (model: string) => { + setInputValue(model); + onChange(model); + setShowSuggestions(false); + + if (validateModel) { + setIsValid(validateModel(model)); + } + }, + [onChange, validateModel], + ); + + return ( +
+ +
+
+ setShowSuggestions(true)} + placeholder={placeholder} + className={`pr-10 ${isValid === false ? 'border-red-500' : isValid === true ? 'border-green-500' : ''}`} + /> + {isValid !== null && ( +
+ {isValid ? ( + + ) : ( + + )} +
+ )} +
+ + {showSuggestions && filteredSuggestions.length > 0 && ( +
+
    + {filteredSuggestions.map((model) => ( +
  • handleSuggestionClick(model)} + > +
    {model}
    +
  • + ))} +
+
+ )} +
+ + {helpText &&

{helpText}

} + + {isValid === false && inputValue && ( +
+
+ + This model name may not be valid for {provider}. Please check the model name. +
+
+ )} +
+ ); +} diff --git a/app/lib/.server/llm/constants.ts b/app/lib/.server/llm/constants.ts index a78b33b0ce..6fbfbc7581 100644 --- a/app/lib/.server/llm/constants.ts +++ b/app/lib/.server/llm/constants.ts @@ -21,7 +21,7 @@ export const PROVIDER_COMPLETION_LIMITS: Record = { Mistral: 8192, Ollama: 8192, OpenRouter: 8192, - Perplexity: 8192, + Perplexity: 127072, // Sonar models support 128k context Together: 8192, xAI: 8192, LMStudio: 8192, diff --git a/app/lib/modules/llm/providers/perplexity-utils.ts b/app/lib/modules/llm/providers/perplexity-utils.ts new file mode 100644 index 0000000000..d48e68a1aa --- /dev/null +++ b/app/lib/modules/llm/providers/perplexity-utils.ts @@ -0,0 +1,170 @@ +/** + * Perplexity Model Validation and Management Utilities + * Author: Keoma Wright + * Purpose: Provides validation and management utilities for Perplexity AI models + */ + +export interface PerplexityModelInfo { + id: string; + name: string; + context: number; + category: 'search' | 'reasoning' | 'research' | 'chat'; + deprecated?: boolean; + replacement?: string; +} + +// Comprehensive list of Perplexity models with metadata +export const PERPLEXITY_MODELS: PerplexityModelInfo[] = [ + // Current generation models (2025) + { + id: 'sonar', + name: 'Sonar (Latest)', + context: 127072, + category: 'search', + }, + { + id: 'sonar-reasoning', + name: 'Sonar Reasoning', + context: 127072, + category: 'reasoning', + }, + { + id: 'sonar-deep-research', + name: 'Sonar Deep Research', + context: 127072, + category: 'research', + }, + + // Llama-based models (still supported) + { + id: 'llama-3.1-sonar-small-128k-online', + name: 'Llama 3.1 Sonar Small (Online)', + context: 127072, + category: 'search', + }, + { + id: 'llama-3.1-sonar-large-128k-online', + name: 'Llama 3.1 Sonar Large (Online)', + context: 127072, + category: 'search', + }, + { + id: 'llama-3.1-sonar-small-128k-chat', + name: 'Llama 3.1 Sonar Small (Chat)', + context: 127072, + category: 'chat', + }, + { + id: 'llama-3.1-sonar-large-128k-chat', + name: 'Llama 3.1 Sonar Large (Chat)', + context: 127072, + category: 'chat', + }, + + // Deprecated models (for backward compatibility) + { + id: 'sonar-pro', + name: 'Sonar Pro (Deprecated)', + context: 8192, + category: 'search', + deprecated: true, + replacement: 'sonar', + }, + { + id: 'sonar-reasoning-pro', + name: 'Sonar Reasoning Pro (Deprecated)', + context: 8192, + category: 'reasoning', + deprecated: true, + replacement: 'sonar-reasoning', + }, +]; + +/** + * Validates if a model ID is supported by Perplexity + */ +export function validatePerplexityModel(modelId: string): boolean { + return PERPLEXITY_MODELS.some((model) => model.id === modelId); +} + +/** + * Gets model information by ID + */ +export function getPerplexityModelInfo(modelId: string): PerplexityModelInfo | undefined { + return PERPLEXITY_MODELS.find((model) => model.id === modelId); +} + +/** + * Gets non-deprecated models + */ +export function getActivePerplexityModels(): PerplexityModelInfo[] { + return PERPLEXITY_MODELS.filter((model) => !model.deprecated); +} + +/** + * Gets model suggestions based on partial input + */ +export function getPerplexityModelSuggestions(partial: string): PerplexityModelInfo[] { + const lowerPartial = partial.toLowerCase(); + return PERPLEXITY_MODELS.filter( + (model) => model.id.toLowerCase().includes(lowerPartial) || model.name.toLowerCase().includes(lowerPartial), + ); +} + +/** + * Checks if a model is deprecated and returns replacement info + */ +export function checkDeprecatedModel(modelId: string): { + deprecated: boolean; + replacement?: string; + message?: string; +} { + const model = getPerplexityModelInfo(modelId); + + if (!model) { + return { deprecated: false }; + } + + if (model.deprecated) { + return { + deprecated: true, + replacement: model.replacement, + message: `Model "${modelId}" is deprecated. Please use "${model.replacement}" instead.`, + }; + } + + return { deprecated: false }; +} + +/** + * Groups models by category + */ +export function getPerplexityModelsByCategory(): Record { + return PERPLEXITY_MODELS.reduce( + (acc, model) => { + if (!acc[model.category]) { + acc[model.category] = []; + } + + acc[model.category].push(model); + + return acc; + }, + {} as Record, + ); +} + +/** + * Pattern matching for flexible model validation + */ +export const PERPLEXITY_MODEL_PATTERNS = [ + /^sonar(-\w+)?$/, + /^llama-\d+(\.\d+)?-sonar-(small|large)-\d+k-(online|chat)$/, +]; + +/** + * Flexible validation that accepts patterns + */ +export function isValidPerplexityModelPattern(modelId: string): boolean { + return PERPLEXITY_MODEL_PATTERNS.some((pattern) => pattern.test(modelId)); +} diff --git a/app/lib/modules/llm/providers/perplexity.ts b/app/lib/modules/llm/providers/perplexity.ts index 8d98affaa0..dfb7ad3b4f 100644 --- a/app/lib/modules/llm/providers/perplexity.ts +++ b/app/lib/modules/llm/providers/perplexity.ts @@ -3,6 +3,12 @@ import type { ModelInfo } from '~/lib/modules/llm/types'; import type { IProviderSetting } from '~/types/model'; import type { LanguageModelV1 } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; +import { + validatePerplexityModel, + getActivePerplexityModels, + checkDeprecatedModel, + isValidPerplexityModelPattern, +} from './perplexity-utils'; export default class PerplexityProvider extends BaseProvider { name = 'Perplexity'; @@ -12,26 +18,37 @@ export default class PerplexityProvider extends BaseProvider { apiTokenKey: 'PERPLEXITY_API_KEY', }; - staticModels: ModelInfo[] = [ - { - name: 'sonar', - label: 'Sonar', - provider: 'Perplexity', - maxTokenAllowed: 8192, - }, - { - name: 'sonar-pro', - label: 'Sonar Pro', - provider: 'Perplexity', - maxTokenAllowed: 8192, - }, - { - name: 'sonar-reasoning-pro', - label: 'Sonar Reasoning Pro', - provider: 'Perplexity', - maxTokenAllowed: 8192, - }, - ]; + // Get models from utility + staticModels: ModelInfo[] = getActivePerplexityModels().map((model) => ({ + name: model.id, + label: model.name, + provider: 'Perplexity', + maxTokenAllowed: model.context, + })); + + // Validate if a model name is supported + isValidModel(modelName: string): boolean { + // First check exact matches, then patterns + return validatePerplexityModel(modelName) || isValidPerplexityModelPattern(modelName); + } + + // Get dynamic models (override base class method) + async getDynamicModels( + _apiKeys?: Record, + _settings?: IProviderSetting, + _serverEnv?: Record, + ): Promise { + try { + /* + * For now, return static models, but this can be extended + * to fetch models from Perplexity API when they provide an endpoint + */ + return this.staticModels; + } catch (error) { + console.warn('Failed to fetch dynamic Perplexity models:', error); + return this.staticModels; + } + } getModelInstance(options: { model: string; @@ -41,7 +58,25 @@ export default class PerplexityProvider extends BaseProvider { }): LanguageModelV1 { const { model, serverEnv, apiKeys, providerSettings } = options; - const { apiKey } = this.getProviderBaseUrlAndKey({ + // Check for deprecated models + const deprecationCheck = checkDeprecatedModel(model); + + if (deprecationCheck.deprecated) { + console.warn(deprecationCheck.message); + + /* + * Optionally use the replacement model + * model = deprecationCheck.replacement || model; + */ + } + + // Validate model before attempting to use it + if (!this.isValidModel(model)) { + const validModels = this.staticModels.map((m) => m.name).join(', '); + throw new Error(`Invalid Perplexity model: "${model}". Valid models are: ${validModels}`); + } + + const { apiKey, baseUrl } = this.getProviderBaseUrlAndKey({ apiKeys, providerSettings: providerSettings?.[this.name], serverEnv: serverEnv as any, @@ -54,7 +89,7 @@ export default class PerplexityProvider extends BaseProvider { } const perplexity = createOpenAI({ - baseURL: 'https://api.perplexity.ai/', + baseURL: baseUrl || 'https://api.perplexity.ai/', apiKey, });