feat(models): Implement model visibility management with hidden models configuration
This commit is contained in:
parent
e47307d1d4
commit
18fdb192d8
8 changed files with 459 additions and 22 deletions
|
|
@ -235,6 +235,11 @@ This fork adds several enhancements to the original Perplexica project:
|
||||||
- ✅ Toggle for automatic suggestions
|
- ✅ Toggle for automatic suggestions
|
||||||
- ✅ Added support for latest Anthropic models
|
- ✅ Added support for latest Anthropic models
|
||||||
- ✅ Adds support for multiple user-customizable system prompt enhancement and personas so you can tailor output to your needs
|
- ✅ Adds support for multiple user-customizable system prompt enhancement and personas so you can tailor output to your needs
|
||||||
|
- ✅ **Model Visibility Management**: Server administrators can hide specific models from the user interface and API responses
|
||||||
|
- Hide expensive models to prevent accidental usage and cost overruns
|
||||||
|
- Remove non-functional or problematic models from user selection
|
||||||
|
- Configurable via settings UI with collapsible provider interface for better organization
|
||||||
|
- API support with `include_hidden` parameter for administrative access
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|
|
||||||
154
docs/installation/configuration.md
Normal file
154
docs/installation/configuration.md
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Configuration Guide
|
||||||
|
|
||||||
|
This guide covers all the configuration options available in Perplexica's `config.toml` file.
|
||||||
|
|
||||||
|
## Configuration File Structure
|
||||||
|
|
||||||
|
Perplexica uses a TOML configuration file to manage settings. Copy `sample.config.toml` to `config.toml` and modify it according to your needs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp sample.config.toml config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Sections
|
||||||
|
|
||||||
|
### [GENERAL]
|
||||||
|
|
||||||
|
General application settings.
|
||||||
|
|
||||||
|
#### SIMILARITY_MEASURE
|
||||||
|
- **Type**: String
|
||||||
|
- **Options**: `"cosine"` or `"dot"`
|
||||||
|
- **Default**: `"cosine"`
|
||||||
|
- **Description**: The similarity measure used for embedding comparisons in search results ranking.
|
||||||
|
|
||||||
|
#### KEEP_ALIVE
|
||||||
|
- **Type**: String
|
||||||
|
- **Default**: `"5m"`
|
||||||
|
- **Description**: How long to keep Ollama models loaded into memory. Use time suffixes like `"5m"` for 5 minutes, `"1h"` for 1 hour, or `"-1m"` for indefinite.
|
||||||
|
|
||||||
|
#### BASE_URL
|
||||||
|
- **Type**: String
|
||||||
|
- **Default**: `""` (empty)
|
||||||
|
- **Description**: Optional base URL override. When set, overrides the detected URL for OpenSearch and other public URLs.
|
||||||
|
|
||||||
|
#### HIDDEN_MODELS
|
||||||
|
- **Type**: Array of Strings
|
||||||
|
- **Default**: `[]` (empty array)
|
||||||
|
- **Description**: Array of model names to hide from the user interface and API responses. Hidden models will not appear in model selection lists but can still be used if directly specified.
|
||||||
|
- **Example**: `["gpt-4", "claude-3-opus", "expensive-model"]`
|
||||||
|
- **Use Cases**:
|
||||||
|
- Hide expensive models to prevent accidental usage
|
||||||
|
- Remove models that don't work well with your configuration
|
||||||
|
- Simplify the UI by hiding unused models
|
||||||
|
|
||||||
|
### [MODELS]
|
||||||
|
|
||||||
|
Model provider configurations. Each provider has its own subsection.
|
||||||
|
|
||||||
|
#### [MODELS.OPENAI]
|
||||||
|
- **API_KEY**: Your OpenAI API key
|
||||||
|
|
||||||
|
#### [MODELS.GROQ]
|
||||||
|
- **API_KEY**: Your Groq API key
|
||||||
|
|
||||||
|
#### [MODELS.ANTHROPIC]
|
||||||
|
- **API_KEY**: Your Anthropic API key
|
||||||
|
|
||||||
|
#### [MODELS.GEMINI]
|
||||||
|
- **API_KEY**: Your Google Gemini API key
|
||||||
|
|
||||||
|
#### [MODELS.CUSTOM_OPENAI]
|
||||||
|
Configuration for OpenAI-compatible APIs (like LMStudio, vLLM, etc.)
|
||||||
|
- **API_KEY**: API key for the custom endpoint
|
||||||
|
- **API_URL**: Base URL for the OpenAI-compatible API
|
||||||
|
- **MODEL_NAME**: Name of the model to use
|
||||||
|
|
||||||
|
#### [MODELS.OLLAMA]
|
||||||
|
- **API_URL**: Ollama server URL (e.g., `"http://host.docker.internal:11434"`)
|
||||||
|
|
||||||
|
#### [MODELS.DEEPSEEK]
|
||||||
|
- **API_KEY**: Your DeepSeek API key
|
||||||
|
|
||||||
|
#### [MODELS.LM_STUDIO]
|
||||||
|
- **API_URL**: LM Studio server URL (e.g., `"http://host.docker.internal:1234"`)
|
||||||
|
|
||||||
|
### [API_ENDPOINTS]
|
||||||
|
|
||||||
|
External service endpoints.
|
||||||
|
|
||||||
|
#### SEARXNG
|
||||||
|
- **Type**: String
|
||||||
|
- **Description**: SearxNG API URL for web search functionality
|
||||||
|
- **Example**: `"http://localhost:32768"`
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Some configurations can also be set via environment variables, which take precedence over the config file:
|
||||||
|
|
||||||
|
- `OPENAI_API_KEY`
|
||||||
|
- `GROQ_API_KEY`
|
||||||
|
- `ANTHROPIC_API_KEY`
|
||||||
|
- `GEMINI_API_KEY`
|
||||||
|
- And others following the pattern `{PROVIDER}_API_KEY`
|
||||||
|
|
||||||
|
## Model Visibility Management
|
||||||
|
|
||||||
|
The `HIDDEN_MODELS` setting allows server administrators to control which models are visible to users:
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
1. Models listed in `HIDDEN_MODELS` are filtered out of API responses
|
||||||
|
2. The settings UI shows all models (including hidden ones) for management
|
||||||
|
3. Hidden models can still be used if explicitly specified in API calls
|
||||||
|
|
||||||
|
### Managing Hidden Models
|
||||||
|
1. **Via Configuration File**: Edit the `HIDDEN_MODELS` array in `config.toml`
|
||||||
|
2. **Via Settings UI**: Use the "Model Visibility" section in the settings page
|
||||||
|
3. **Via API**: Use the `/api/config` endpoint to update the configuration
|
||||||
|
|
||||||
|
### API Behavior
|
||||||
|
- **Default**: `/api/models` returns only visible models
|
||||||
|
- **Include Hidden**: `/api/models?include_hidden=true` returns all models (for admin use)
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Store API keys securely and never commit them to version control
|
||||||
|
- Use environment variables for sensitive configuration in production
|
||||||
|
- Regularly rotate API keys
|
||||||
|
- Consider using `HIDDEN_MODELS` to prevent access to expensive or sensitive models
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Models not appearing**: Check if they're listed in `HIDDEN_MODELS`
|
||||||
|
2. **API errors**: Verify API keys and URLs are correct
|
||||||
|
3. **Ollama connection issues**: Ensure the Ollama server is running and accessible
|
||||||
|
4. **SearxNG not working**: Verify the SearxNG endpoint is correct and accessible
|
||||||
|
|
||||||
|
### Configuration Validation
|
||||||
|
|
||||||
|
The application validates configuration on startup and will log errors for:
|
||||||
|
- Invalid TOML syntax
|
||||||
|
- Missing required fields
|
||||||
|
- Invalid URLs or API endpoints
|
||||||
|
- Unreachable services
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[GENERAL]
|
||||||
|
SIMILARITY_MEASURE = "cosine"
|
||||||
|
KEEP_ALIVE = "5m"
|
||||||
|
BASE_URL = ""
|
||||||
|
HIDDEN_MODELS = ["gpt-4", "claude-3-opus"]
|
||||||
|
|
||||||
|
[MODELS.OPENAI]
|
||||||
|
API_KEY = "sk-your-openai-key-here"
|
||||||
|
|
||||||
|
[MODELS.OLLAMA]
|
||||||
|
API_URL = "http://localhost:11434"
|
||||||
|
|
||||||
|
[API_ENDPOINTS]
|
||||||
|
SEARXNG = "http://localhost:32768"
|
||||||
|
```
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
|
||||||
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
|
||||||
BASE_URL = "" # Optional. When set, overrides detected URL for OpenSearch and other public URLs
|
BASE_URL = "" # Optional. When set, overrides detected URL for OpenSearch and other public URLs
|
||||||
|
HIDDEN_MODELS = [] # Array of model names to hide from the user interface (e.g., ["gpt-4", "claude-3-opus"])
|
||||||
|
|
||||||
[MODELS.OPENAI]
|
[MODELS.OPENAI]
|
||||||
API_KEY = ""
|
API_KEY = ""
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
getOpenaiApiKey,
|
getOpenaiApiKey,
|
||||||
getDeepseekApiKey,
|
getDeepseekApiKey,
|
||||||
getLMStudioApiEndpoint,
|
getLMStudioApiEndpoint,
|
||||||
|
getHiddenModels,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
} from '@/lib/config';
|
} from '@/lib/config';
|
||||||
import {
|
import {
|
||||||
|
|
@ -70,6 +71,7 @@ export const GET = async (req: Request) => {
|
||||||
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
config['customOpenaiApiUrl'] = getCustomOpenaiApiUrl();
|
||||||
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
config['customOpenaiModelName'] = getCustomOpenaiModelName();
|
||||||
config['baseUrl'] = getBaseUrl();
|
config['baseUrl'] = getBaseUrl();
|
||||||
|
config['hiddenModels'] = getHiddenModels();
|
||||||
|
|
||||||
return Response.json({ ...config }, { status: 200 });
|
return Response.json({ ...config }, { status: 200 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -96,6 +98,9 @@ export const POST = async (req: Request) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedConfig = {
|
const updatedConfig = {
|
||||||
|
GENERAL: {
|
||||||
|
HIDDEN_MODELS: config.hiddenModels || [],
|
||||||
|
},
|
||||||
MODELS: {
|
MODELS: {
|
||||||
OPENAI: {
|
OPENAI: {
|
||||||
API_KEY: getUpdatedProtectedValue(
|
API_KEY: getUpdatedProtectedValue(
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ import {
|
||||||
|
|
||||||
export const GET = async (req: Request) => {
|
export const GET = async (req: Request) => {
|
||||||
try {
|
try {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const includeHidden = url.searchParams.get('include_hidden') === 'true';
|
||||||
|
|
||||||
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
const [chatModelProviders, embeddingModelProviders] = await Promise.all([
|
||||||
getAvailableChatModelProviders(),
|
getAvailableChatModelProviders({ includeHidden }),
|
||||||
getAvailableEmbeddingModelProviders(),
|
getAvailableEmbeddingModelProviders({ includeHidden }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Object.keys(chatModelProviders).forEach((provider) => {
|
Object.keys(chatModelProviders).forEach((provider) => {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import {
|
||||||
Save,
|
Save,
|
||||||
X,
|
X,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
@ -40,6 +42,7 @@ interface SettingsType {
|
||||||
customOpenaiApiUrl: string;
|
customOpenaiApiUrl: string;
|
||||||
customOpenaiModelName: string;
|
customOpenaiModelName: string;
|
||||||
ollamaContextWindow: number;
|
ollamaContextWindow: number;
|
||||||
|
hiddenModels: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
|
@ -245,6 +248,14 @@ export default function SettingsPage() {
|
||||||
);
|
);
|
||||||
const [isAddingNewPrompt, setIsAddingNewPrompt] = useState(false);
|
const [isAddingNewPrompt, setIsAddingNewPrompt] = useState(false);
|
||||||
|
|
||||||
|
// Model visibility state variables
|
||||||
|
const [allModels, setAllModels] = useState<{
|
||||||
|
chat: Record<string, Record<string, any>>;
|
||||||
|
embedding: Record<string, Record<string, any>>;
|
||||||
|
}>({ chat: {}, embedding: {} });
|
||||||
|
const [hiddenModels, setHiddenModels] = useState<string[]>([]);
|
||||||
|
const [expandedProviders, setExpandedProviders] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Default Search Settings state variables
|
// Default Search Settings state variables
|
||||||
const [searchOptimizationMode, setSearchOptimizationMode] =
|
const [searchOptimizationMode, setSearchOptimizationMode] =
|
||||||
useState<string>('');
|
useState<string>('');
|
||||||
|
|
@ -265,6 +276,9 @@ export default function SettingsPage() {
|
||||||
|
|
||||||
setConfig(data);
|
setConfig(data);
|
||||||
|
|
||||||
|
// Populate hiddenModels state from config
|
||||||
|
setHiddenModels(data.hiddenModels || []);
|
||||||
|
|
||||||
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||||
const embeddingModelProvidersKeys = Object.keys(
|
const embeddingModelProvidersKeys = Object.keys(
|
||||||
data.embeddingModelProviders || {},
|
data.embeddingModelProviders || {},
|
||||||
|
|
@ -319,7 +333,29 @@ export default function SettingsPage() {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchAllModels = async () => {
|
||||||
|
try {
|
||||||
|
// Fetch complete model list including hidden models
|
||||||
|
const res = await fetch(`/api/models?include_hidden=true`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setAllModels({
|
||||||
|
chat: data.chatModelProviders || {},
|
||||||
|
embedding: data.embeddingModelProviders || {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch all models:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
|
fetchAllModels();
|
||||||
|
|
||||||
// Load search settings from localStorage
|
// Load search settings from localStorage
|
||||||
const loadSearchSettings = () => {
|
const loadSearchSettings = () => {
|
||||||
|
|
@ -529,6 +565,42 @@ export default function SettingsPage() {
|
||||||
localStorage.setItem(key, value);
|
localStorage.setItem(key, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleModelVisibilityToggle = async (modelKey: string, isVisible: boolean) => {
|
||||||
|
let updatedHiddenModels: string[];
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
// Model should be visible, remove from hidden list
|
||||||
|
updatedHiddenModels = hiddenModels.filter(m => m !== modelKey);
|
||||||
|
} else {
|
||||||
|
// Model should be hidden, add to hidden list
|
||||||
|
updatedHiddenModels = [...hiddenModels, modelKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local state immediately
|
||||||
|
setHiddenModels(updatedHiddenModels);
|
||||||
|
|
||||||
|
// Persist changes to backend
|
||||||
|
try {
|
||||||
|
await saveConfig('hiddenModels', updatedHiddenModels);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save hidden models:', error);
|
||||||
|
// Revert local state on error
|
||||||
|
setHiddenModels(hiddenModels);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleProviderExpansion = (providerId: string) => {
|
||||||
|
setExpandedProviders(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(providerId)) {
|
||||||
|
newSet.delete(providerId);
|
||||||
|
} else {
|
||||||
|
newSet.add(providerId);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddOrUpdateSystemPrompt = async () => {
|
const handleAddOrUpdateSystemPrompt = async () => {
|
||||||
const currentPrompt = editingPrompt || {
|
const currentPrompt = editingPrompt || {
|
||||||
name: newPromptName,
|
name: newPromptName,
|
||||||
|
|
@ -1400,6 +1472,123 @@ export default function SettingsPage() {
|
||||||
)}
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection
|
||||||
|
title="Model Visibility"
|
||||||
|
tooltip="Hide models from the API to prevent them from appearing in model lists.\nHidden models will not be available for selection in the interface.\nThis allows server admins to disable models that may incur large costs or won't work with the application."
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
{/* Unified Models List */}
|
||||||
|
{(() => {
|
||||||
|
// Combine all models from both chat and embedding providers
|
||||||
|
const allProviders: Record<string, Record<string, any>> = {};
|
||||||
|
|
||||||
|
// Add chat models
|
||||||
|
Object.entries(allModels.chat).forEach(([provider, models]) => {
|
||||||
|
if (!allProviders[provider]) {
|
||||||
|
allProviders[provider] = {};
|
||||||
|
}
|
||||||
|
Object.entries(models).forEach(([modelKey, model]) => {
|
||||||
|
allProviders[provider][modelKey] = model;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add embedding models
|
||||||
|
Object.entries(allModels.embedding).forEach(([provider, models]) => {
|
||||||
|
if (!allProviders[provider]) {
|
||||||
|
allProviders[provider] = {};
|
||||||
|
}
|
||||||
|
Object.entries(models).forEach(([modelKey, model]) => {
|
||||||
|
allProviders[provider][modelKey] = model;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(allProviders).length > 0 ? (
|
||||||
|
Object.entries(allProviders).map(([provider, models]) => {
|
||||||
|
const providerId = `provider-${provider}`;
|
||||||
|
const isExpanded = expandedProviders.has(providerId);
|
||||||
|
const modelEntries = Object.entries(models);
|
||||||
|
const hiddenCount = modelEntries.filter(([modelKey]) => hiddenModels.includes(modelKey)).length;
|
||||||
|
const totalCount = modelEntries.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={providerId}
|
||||||
|
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleProviderExpansion(providerId)}
|
||||||
|
className="w-full p-3 bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition-colors flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown size={16} className="text-black/70 dark:text-white/70" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={16} className="text-black/70 dark:text-white/70" />
|
||||||
|
)}
|
||||||
|
<h4 className="text-sm font-medium text-black/80 dark:text-white/80">
|
||||||
|
{(PROVIDER_METADATA as any)[provider]?.displayName ||
|
||||||
|
provider.charAt(0).toUpperCase() + provider.slice(1)}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-black/60 dark:text-white/60">
|
||||||
|
<span>{totalCount - hiddenCount} visible</span>
|
||||||
|
{hiddenCount > 0 && (
|
||||||
|
<span className="px-2 py-1 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded">
|
||||||
|
{hiddenCount} hidden
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="p-3 bg-light-100 dark:bg-dark-100 border-t border-light-200 dark:border-dark-200">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{modelEntries.map(([modelKey, model]) => (
|
||||||
|
<div
|
||||||
|
key={`${provider}-${modelKey}`}
|
||||||
|
className="flex items-center justify-between p-2 bg-white dark:bg-dark-secondary rounded-md"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-black/90 dark:text-white/90">
|
||||||
|
{model.displayName || modelKey}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={!hiddenModels.includes(modelKey)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
handleModelVisibilityToggle(modelKey, checked);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
!hiddenModels.includes(modelKey)
|
||||||
|
? 'bg-[#24A0ED]'
|
||||||
|
: 'bg-light-200 dark:bg-dark-200',
|
||||||
|
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
!hiddenModels.includes(modelKey)
|
||||||
|
? 'translate-x-5'
|
||||||
|
: 'translate-x-1',
|
||||||
|
'inline-block h-3 w-3 transform rounded-full bg-white transition-transform',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-black/60 dark:text-white/60 italic">
|
||||||
|
No models available
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title="API Keys"
|
title="API Keys"
|
||||||
tooltip="API Key values can be viewed in the config.toml file"
|
tooltip="API Key values can be viewed in the config.toml file"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface Config {
|
||||||
SIMILARITY_MEASURE: string;
|
SIMILARITY_MEASURE: string;
|
||||||
KEEP_ALIVE: string;
|
KEEP_ALIVE: string;
|
||||||
BASE_URL?: string;
|
BASE_URL?: string;
|
||||||
|
HIDDEN_MODELS: string[];
|
||||||
};
|
};
|
||||||
MODELS: {
|
MODELS: {
|
||||||
OPENAI: {
|
OPENAI: {
|
||||||
|
|
@ -57,9 +58,35 @@ type RecursivePartial<T> = {
|
||||||
const loadConfig = () => {
|
const loadConfig = () => {
|
||||||
// Server-side only
|
// Server-side only
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return toml.parse(
|
const config = toml.parse(
|
||||||
fs.readFileSync(path.join(process.cwd(), `${configFileName}`), 'utf-8'),
|
fs.readFileSync(path.join(process.cwd(), `${configFileName}`), 'utf-8'),
|
||||||
) as any as Config;
|
) as any as Config;
|
||||||
|
|
||||||
|
// Ensure GENERAL section exists
|
||||||
|
if (!config.GENERAL) {
|
||||||
|
config.GENERAL = {} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HIDDEN_MODELS - fix malformed table format to proper array
|
||||||
|
if (!config.GENERAL.HIDDEN_MODELS) {
|
||||||
|
config.GENERAL.HIDDEN_MODELS = [];
|
||||||
|
} else if (typeof config.GENERAL.HIDDEN_MODELS === 'object' && !Array.isArray(config.GENERAL.HIDDEN_MODELS)) {
|
||||||
|
// Convert malformed table format to array
|
||||||
|
const hiddenModelsObj = config.GENERAL.HIDDEN_MODELS as any;
|
||||||
|
const hiddenModelsArray: string[] = [];
|
||||||
|
|
||||||
|
// Extract values from numeric keys and sort by key
|
||||||
|
const keys = Object.keys(hiddenModelsObj).map(k => parseInt(k)).filter(k => !isNaN(k)).sort((a, b) => a - b);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (typeof hiddenModelsObj[key] === 'string') {
|
||||||
|
hiddenModelsArray.push(hiddenModelsObj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.GENERAL.HIDDEN_MODELS = hiddenModelsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-side fallback - settings will be loaded via API
|
// Client-side fallback - settings will be loaded via API
|
||||||
|
|
@ -73,6 +100,8 @@ export const getKeepAlive = () => loadConfig().GENERAL.KEEP_ALIVE;
|
||||||
|
|
||||||
export const getBaseUrl = () => loadConfig().GENERAL.BASE_URL;
|
export const getBaseUrl = () => loadConfig().GENERAL.BASE_URL;
|
||||||
|
|
||||||
|
export const getHiddenModels = () => loadConfig().GENERAL.HIDDEN_MODELS;
|
||||||
|
|
||||||
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
|
export const getOpenaiApiKey = () => loadConfig().MODELS.OPENAI.API_KEY;
|
||||||
|
|
||||||
export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY;
|
export const getGroqApiKey = () => loadConfig().MODELS.GROQ.API_KEY;
|
||||||
|
|
@ -109,17 +138,26 @@ const mergeConfigs = (current: any, update: any): any => {
|
||||||
return update;
|
return update;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle arrays specifically - don't merge them, replace them
|
||||||
|
if (Array.isArray(update)) {
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
const result = { ...current };
|
const result = { ...current };
|
||||||
|
|
||||||
for (const key in update) {
|
for (const key in update) {
|
||||||
if (Object.prototype.hasOwnProperty.call(update, key)) {
|
if (Object.prototype.hasOwnProperty.call(update, key)) {
|
||||||
const updateValue = update[key];
|
const updateValue = update[key];
|
||||||
|
|
||||||
if (
|
// Handle arrays specifically - don't merge them, replace them
|
||||||
|
if (Array.isArray(updateValue)) {
|
||||||
|
result[key] = updateValue;
|
||||||
|
} else if (
|
||||||
typeof updateValue === 'object' &&
|
typeof updateValue === 'object' &&
|
||||||
updateValue !== null &&
|
updateValue !== null &&
|
||||||
typeof result[key] === 'object' &&
|
typeof result[key] === 'object' &&
|
||||||
result[key] !== null
|
result[key] !== null &&
|
||||||
|
!Array.isArray(result[key])
|
||||||
) {
|
) {
|
||||||
result[key] = mergeConfigs(result[key], updateValue);
|
result[key] = mergeConfigs(result[key], updateValue);
|
||||||
} else if (updateValue !== undefined) {
|
} else if (updateValue !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
getCustomOpenaiApiKey,
|
getCustomOpenaiApiKey,
|
||||||
getCustomOpenaiApiUrl,
|
getCustomOpenaiApiUrl,
|
||||||
getCustomOpenaiModelName,
|
getCustomOpenaiModelName,
|
||||||
|
getHiddenModels,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
import {
|
import {
|
||||||
|
|
@ -90,7 +91,10 @@ export const embeddingModelProviders: Record<
|
||||||
lmstudio: loadLMStudioEmbeddingsModels,
|
lmstudio: loadLMStudioEmbeddingsModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAvailableChatModelProviders = async () => {
|
export const getAvailableChatModelProviders = async (
|
||||||
|
options: { includeHidden?: boolean } = {}
|
||||||
|
) => {
|
||||||
|
const { includeHidden = false } = options;
|
||||||
const models: Record<string, Record<string, ChatModel>> = {};
|
const models: Record<string, Record<string, ChatModel>> = {};
|
||||||
|
|
||||||
for (const provider in chatModelProviders) {
|
for (const provider in chatModelProviders) {
|
||||||
|
|
@ -111,28 +115,48 @@ export const getAvailableChatModelProviders = async () => {
|
||||||
const customOpenAiApiUrl = getCustomOpenaiApiUrl();
|
const customOpenAiApiUrl = getCustomOpenaiApiUrl();
|
||||||
const customOpenAiModelName = getCustomOpenaiModelName();
|
const customOpenAiModelName = getCustomOpenaiModelName();
|
||||||
|
|
||||||
models['custom_openai'] = {
|
// Only add custom_openai provider if all required fields are configured
|
||||||
...(customOpenAiApiKey && customOpenAiApiUrl && customOpenAiModelName
|
if (customOpenAiApiKey && customOpenAiApiUrl && customOpenAiModelName) {
|
||||||
? {
|
models['custom_openai'] = {
|
||||||
[customOpenAiModelName]: {
|
[customOpenAiModelName]: {
|
||||||
displayName: customOpenAiModelName,
|
displayName: customOpenAiModelName,
|
||||||
model: new ChatOpenAI({
|
model: new ChatOpenAI({
|
||||||
openAIApiKey: customOpenAiApiKey,
|
openAIApiKey: customOpenAiApiKey,
|
||||||
modelName: customOpenAiModelName,
|
modelName: customOpenAiModelName,
|
||||||
// temperature: 0.7,
|
// temperature: 0.7,
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL: customOpenAiApiUrl,
|
baseURL: customOpenAiApiUrl,
|
||||||
},
|
|
||||||
}) as unknown as BaseChatModel,
|
|
||||||
},
|
},
|
||||||
|
}) as unknown as BaseChatModel,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out hidden models if includeHidden is false
|
||||||
|
if (!includeHidden) {
|
||||||
|
const hiddenModels = getHiddenModels();
|
||||||
|
if (hiddenModels.length > 0) {
|
||||||
|
for (const provider in models) {
|
||||||
|
for (const modelKey in models[provider]) {
|
||||||
|
if (hiddenModels.includes(modelKey)) {
|
||||||
|
delete models[provider][modelKey];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
: {}),
|
// Remove provider if all models are hidden
|
||||||
};
|
if (Object.keys(models[provider]).length === 0) {
|
||||||
|
delete models[provider];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAvailableEmbeddingModelProviders = async () => {
|
export const getAvailableEmbeddingModelProviders = async (
|
||||||
|
options: { includeHidden?: boolean } = {}
|
||||||
|
) => {
|
||||||
|
const { includeHidden = false } = options;
|
||||||
const models: Record<string, Record<string, EmbeddingModel>> = {};
|
const models: Record<string, Record<string, EmbeddingModel>> = {};
|
||||||
|
|
||||||
for (const provider in embeddingModelProviders) {
|
for (const provider in embeddingModelProviders) {
|
||||||
|
|
@ -149,5 +173,23 @@ export const getAvailableEmbeddingModelProviders = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out hidden models if includeHidden is false
|
||||||
|
if (!includeHidden) {
|
||||||
|
const hiddenModels = getHiddenModels();
|
||||||
|
if (hiddenModels.length > 0) {
|
||||||
|
for (const provider in models) {
|
||||||
|
for (const modelKey in models[provider]) {
|
||||||
|
if (hiddenModels.includes(modelKey)) {
|
||||||
|
delete models[provider][modelKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove provider if all models are hidden
|
||||||
|
if (Object.keys(models[provider]).length === 0) {
|
||||||
|
delete models[provider];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue