From 283eb22ae57cfd2341fc5a2748f2f4b5d8107ef1 Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Wed, 18 Dec 2024 20:04:43 +0530 Subject: [PATCH] added indicator on settings menu --- app/commit.json | 2 +- app/components/settings/debug/DebugTab.tsx | 17 +- .../settings/providers/ProvidersTab.tsx | 119 ++++++----- app/lib/.server/llm/api-key.ts | 29 ++- app/types/model.ts | 1 + app/utils/constants.ts | 198 ++++++++++++------ vite.config.ts | 2 +- 7 files changed, 237 insertions(+), 131 deletions(-) diff --git a/app/commit.json b/app/commit.json index 4ff52947d..b0a4c9293 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "fce8999f27c0affbc762dc90de992b5a759ab325" } +{ "commit": "62ebfe51a69788229aa62d34afb4ce89b7cd8ac8" } diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx index cf2341b55..b0cde7dd7 100644 --- a/app/components/settings/debug/DebugTab.tsx +++ b/app/components/settings/debug/DebugTab.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useSettings } from '~/lib/hooks/useSettings'; import commit from '~/commit.json'; import { toast } from 'react-toastify'; +import { providerBaseUrlEnvKeys } from '~/utils/constants'; interface ProviderStatus { name: string; @@ -236,7 +237,7 @@ const checkProviderStatus = async (url: string | null, providerName: string): Pr } // Try different endpoints based on provider - const checkUrls = [`${url}/api/health`, `${url}/v1/models`]; + const checkUrls = [`${url}/api/health`, url.endsWith('v1') ? `${url}/models` : `${url}/v1/models`]; console.log(`[Debug] Checking additional endpoints:`, checkUrls); const results = await Promise.all( @@ -321,14 +322,16 @@ export default function DebugTab() { .filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name)) .map(async ([, provider]) => { const envVarName = - provider.name.toLowerCase() === 'ollama' - ? 'OLLAMA_API_BASE_URL' - : provider.name.toLowerCase() === 'lmstudio' - ? 'LMSTUDIO_API_BASE_URL' - : `REACT_APP_${provider.name.toUpperCase()}_URL`; + providerBaseUrlEnvKeys[provider.name].baseUrlKey || `REACT_APP_${provider.name.toUpperCase()}_URL`; // Access environment variables through import.meta.env - const url = import.meta.env[envVarName] || provider.settings.baseUrl || null; // Ensure baseUrl is used + let settingsUrl = provider.settings.baseUrl; + + if (settingsUrl && settingsUrl.trim().length === 0) { + settingsUrl = undefined; + } + + const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`); const status = await checkProviderStatus(url, provider.name); diff --git a/app/components/settings/providers/ProvidersTab.tsx b/app/components/settings/providers/ProvidersTab.tsx index 49a16f668..20e66efea 100644 --- a/app/components/settings/providers/ProvidersTab.tsx +++ b/app/components/settings/providers/ProvidersTab.tsx @@ -7,6 +7,7 @@ import { logStore } from '~/lib/stores/logs'; // Import a default fallback icon import DefaultIcon from '/icons/Default.svg'; // Adjust the path as necessary +import { providerBaseUrlEnvKeys } from '~/utils/constants'; export default function ProvidersTab() { const { providers, updateProviderSettings, isLocalModel } = useSettings(); @@ -47,65 +48,77 @@ export default function ProvidersTab() { className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" /> - {filteredProviders.map((provider) => ( -
-
-
- { - // Fallback to default icon on error - e.currentTarget.src = DefaultIcon; - }} - alt={`${provider.name} icon`} - className="w-6 h-6 dark:invert" - /> - {provider.name} -
- { - updateProviderSettings(provider.name, { ...provider.settings, enabled }); + {filteredProviders.map((provider) => { + const envBaseUrlKey = providerBaseUrlEnvKeys[provider.name].baseUrlKey; + const envBaseUrl = envBaseUrlKey ? import.meta.env[envBaseUrlKey] : undefined; - if (enabled) { - logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name }); - } else { - logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name }); - } - }} - /> -
- {/* Base URL input for configurable providers */} - {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.settings.enabled && ( -
- - { - let newBaseUrl: string | undefined = e.target.value; + return ( +
+
+
+ { + // Fallback to default icon on error + e.currentTarget.src = DefaultIcon; + }} + alt={`${provider.name} icon`} + className="w-6 h-6 dark:invert" + /> + {provider.name} +
+ { + updateProviderSettings(provider.name, { ...provider.settings, enabled }); - if (newBaseUrl && newBaseUrl.trim().length === 0) { - newBaseUrl = undefined; + if (enabled) { + logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name }); + } else { + logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name }); } - - updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl }); - logStore.logProvider(`Base URL updated for ${provider.name}`, { - provider: provider.name, - baseUrl: newBaseUrl, - }); }} - placeholder={`Enter ${provider.name} base URL`} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" />
- )} -
- ))} + {/* Base URL input for configurable providers */} + {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.settings.enabled && ( +
+ {envBaseUrl && ( + + )} + + { + let newBaseUrl: string | undefined = e.target.value; + + if (newBaseUrl && newBaseUrl.trim().length === 0) { + newBaseUrl = undefined; + } + + updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl }); + logStore.logProvider(`Base URL updated for ${provider.name}`, { + provider: provider.name, + baseUrl: newBaseUrl, + }); + }} + placeholder={`Enter ${provider.name} base URL`} + className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" + /> +
+ )} +
+ ); + })}
); } diff --git a/app/lib/.server/llm/api-key.ts b/app/lib/.server/llm/api-key.ts index d21f07051..83b46462f 100644 --- a/app/lib/.server/llm/api-key.ts +++ b/app/lib/.server/llm/api-key.ts @@ -4,6 +4,7 @@ */ import { env } from 'node:process'; import type { IProviderSetting } from '~/types/model'; +import { getProviderBaseUrlAndKey } from '~/utils/constants'; export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Record) { /** @@ -16,7 +17,20 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re return userApiKeys[provider]; } - // Fall back to environment variables + const { apiKey } = getProviderBaseUrlAndKey({ + provider, + apiKeys: userApiKeys, + providerSettings: undefined, + serverEnv: cloudflareEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: '', + }); + + if (apiKey) { + return apiKey; + } + + // Fall back to hardcoded environment variables names switch (provider) { case 'Anthropic': return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY; @@ -52,6 +66,19 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re } export function getBaseURL(cloudflareEnv: Env, provider: string, providerSettings?: Record) { + const { baseUrl } = getProviderBaseUrlAndKey({ + provider, + apiKeys: {}, + providerSettings, + serverEnv: cloudflareEnv as any, + defaultBaseUrlKey: '', + defaultApiTokenKey: '', + }); + + if (baseUrl) { + return baseUrl; + } + let settingBaseUrl = providerSettings?.[provider].baseUrl; if (settingBaseUrl && settingBaseUrl.length == 0) { diff --git a/app/types/model.ts b/app/types/model.ts index a747a3fb4..b449363b3 100644 --- a/app/types/model.ts +++ b/app/types/model.ts @@ -4,6 +4,7 @@ export type ProviderInfo = { staticModels: ModelInfo[]; name: string; getDynamicModels?: ( + providerName: string, apiKeys?: Record, providerSettings?: IProviderSetting, serverEnv?: Record, diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 6595d9c9f..c4eb0ae0d 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -318,6 +318,83 @@ const PROVIDER_LIST: ProviderInfo[] = [ }, ]; +export const providerBaseUrlEnvKeys: Record = { + Anthropic: { + apiTokenKey: 'ANTHROPIC_API_KEY', + }, + OpenAI: { + apiTokenKey: 'OPENAI_API_KEY', + }, + Groq: { + apiTokenKey: 'GROQ_API_KEY', + }, + HuggingFace: { + apiTokenKey: 'HuggingFace_API_KEY', + }, + OpenRouter: { + apiTokenKey: 'OPEN_ROUTER_API_KEY', + }, + Google: { + apiTokenKey: 'GOOGLE_GENERATIVE_AI_API_KEY', + }, + OpenAILike: { + baseUrlKey: 'OPENAI_LIKE_API_BASE_URL', + apiTokenKey: 'OPENAI_LIKE_API_KEY', + }, + Together: { + baseUrlKey: 'TOGETHER_API_BASE_URL', + apiTokenKey: 'TOGETHER_API_KEY', + }, + Deepseek: { + apiTokenKey: 'DEEPSEEK_API_KEY', + }, + Mistral: { + apiTokenKey: 'MISTRAL_API_KEY', + }, + LMStudio: { + baseUrlKey: 'LMSTUDIO_API_BASE_URL', + }, + xAI: { + apiTokenKey: 'XAI_API_KEY', + }, + Cohere: { + apiTokenKey: 'COHERE_API_KEY', + }, + Perplexity: { + apiTokenKey: 'PERPLEXITY_API_KEY', + }, + Ollama: { + baseUrlKey: 'OLLAMA_API_BASE_URL', + }, +}; + +export const getProviderBaseUrlAndKey = (options: { + provider: string; + apiKeys?: Record; + providerSettings?: IProviderSetting; + serverEnv?: Record; + defaultBaseUrlKey: string; + defaultApiTokenKey: string; +}) => { + const { provider, apiKeys, providerSettings, serverEnv, defaultBaseUrlKey, defaultApiTokenKey } = options; + let settingsBaseUrl = providerSettings?.baseUrl; + + if (settingsBaseUrl && settingsBaseUrl.length == 0) { + settingsBaseUrl = undefined; + } + + const baseUrlKey = providerBaseUrlEnvKeys[provider]?.baseUrlKey || defaultBaseUrlKey; + const baseUrl = settingsBaseUrl || serverEnv?.[baseUrlKey] || process.env[baseUrlKey] || import.meta.env[baseUrlKey]; + + const apiTokenKey = providerBaseUrlEnvKeys[provider]?.apiTokenKey || defaultApiTokenKey; + const apiKey = + apiKeys?.[provider] || serverEnv?.[apiTokenKey] || process.env[apiTokenKey] || import.meta.env[apiTokenKey]; + + return { + baseUrl, + apiKey, + }; +}; export const DEFAULT_PROVIDER = PROVIDER_LIST[0]; const staticModels: ModelInfo[] = PROVIDER_LIST.map((p) => p.staticModels).flat(); @@ -337,7 +414,7 @@ export async function getModelList(options: { await Promise.all( PROVIDER_LIST.filter( (p): p is ProviderInfo & { getDynamicModels: () => Promise } => !!p.getDynamicModels, - ).map((p) => p.getDynamicModels(apiKeys, providerSettings?.[p.name], serverEnv)), + ).map((p) => p.getDynamicModels(p.name, apiKeys, providerSettings?.[p.name], serverEnv)), ) ).flat(), ...staticModels, @@ -347,35 +424,26 @@ export async function getModelList(options: { } async function getTogetherModels( + name: string, apiKeys?: Record, settings?: IProviderSetting, serverEnv: Record = {}, ): Promise { try { - let settingsBaseUrl = settings?.baseUrl; - - if (settingsBaseUrl && settingsBaseUrl.length == 0) { - settingsBaseUrl = undefined; - } - - const baseUrl = - settingsBaseUrl || - serverEnv?.TOGETHER_API_BASE_URL || - process.env.TOGETHER_API_BASE_URL || - import.meta.env.TOGETHER_API_BASE_URL || - ''; - const provider = 'Together'; + const { baseUrl, apiKey } = getProviderBaseUrlAndKey({ + provider: name, + apiKeys, + providerSettings: settings, + serverEnv, + defaultBaseUrlKey: 'TOGETHER_API_BASE_URL', + defaultApiTokenKey: 'TOGETHER_API_KEY', + }); + console.log({ baseUrl, apiKey }); if (!baseUrl) { return []; } - let apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? ''; - - if (apiKeys && apiKeys[provider]) { - apiKey = apiKeys[provider]; - } - if (!apiKey) { return []; } @@ -393,7 +461,7 @@ async function getTogetherModels( label: `${m.display_name} - in:$${m.pricing.input.toFixed( 2, )} out:$${m.pricing.output.toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`, - provider, + provider: name, maxTokenAllowed: 8000, })); } catch (e) { @@ -402,39 +470,40 @@ async function getTogetherModels( } } -const getOllamaBaseUrl = (settings?: IProviderSetting, serverEnv: Record = {}) => { - let settingsBaseUrl = settings?.baseUrl; - - if (settingsBaseUrl && settingsBaseUrl.length == 0) { - settingsBaseUrl = undefined; - } - - const defaultBaseUrl = - settings?.baseUrl || - serverEnv?.OLLAMA_API_BASE_URL || - process.env.OLLAMA_API_BASE_URL || - import.meta.env.OLLAMA_API_BASE_URL || - 'http://localhost:11434'; +const getOllamaBaseUrl = (name: string, settings?: IProviderSetting, serverEnv: Record = {}) => { + const { baseUrl } = getProviderBaseUrlAndKey({ + provider: name, + providerSettings: settings, + serverEnv, + defaultBaseUrlKey: 'OLLAMA_API_BASE_URL', + defaultApiTokenKey: '', + }); // Check if we're in the browser if (typeof window !== 'undefined') { // Frontend always uses localhost - return defaultBaseUrl; + return baseUrl; } // Backend: Check if we're running in Docker const isDocker = process.env.RUNNING_IN_DOCKER === 'true'; - return isDocker ? defaultBaseUrl.replace('localhost', 'host.docker.internal') : defaultBaseUrl; + return isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl; }; async function getOllamaModels( - apiKeys?: Record, + name: string, + _apiKeys?: Record, settings?: IProviderSetting, serverEnv: Record = {}, ): Promise { try { - const baseUrl = getOllamaBaseUrl(settings, serverEnv); + const baseUrl = getOllamaBaseUrl(name, settings, serverEnv); + + if (!baseUrl) { + return []; + } + const response = await fetch(`${baseUrl}/api/tags`); const data = (await response.json()) as OllamaApiResponse; @@ -453,34 +522,25 @@ async function getOllamaModels( } async function getOpenAILikeModels( + name: string, apiKeys?: Record, settings?: IProviderSetting, serverEnv: Record = {}, ): Promise { try { - let settingsBaseUrl = settings?.baseUrl; - - if (settingsBaseUrl && settingsBaseUrl.length == 0) { - settingsBaseUrl = undefined; - } - - const baseUrl = - settingsBaseUrl || - serverEnv.OPENAI_LIKE_API_BASE_URL || - process.env.OPENAI_LIKE_API_BASE_URL || - import.meta.env.OPENAI_LIKE_API_BASE_URL || - ''; + const { baseUrl, apiKey } = getProviderBaseUrlAndKey({ + provider: name, + apiKeys, + providerSettings: settings, + serverEnv, + defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL', + defaultApiTokenKey: 'OPENAI_LIKE_API_KEY', + }); if (!baseUrl) { return []; } - let apiKey = ''; - - if (apiKeys && apiKeys.OpenAILike) { - apiKey = apiKeys.OpenAILike; - } - const response = await fetch(`${baseUrl}/models`, { headers: { Authorization: `Bearer ${apiKey}`, @@ -491,7 +551,7 @@ async function getOpenAILikeModels( return res.data.map((model: any) => ({ name: model.id, label: model.id, - provider: 'OpenAILike', + provider: name, })); } catch (e) { console.error('Error getting OpenAILike models:', e); @@ -533,23 +593,25 @@ async function getOpenRouterModels(): Promise { } async function getLMStudioModels( - _apiKeys?: Record, + name: string, + apiKeys?: Record, settings?: IProviderSetting, serverEnv: Record = {}, ): Promise { try { - let settingsBaseUrl = settings?.baseUrl; + const { baseUrl } = getProviderBaseUrlAndKey({ + provider: name, + apiKeys, + providerSettings: settings, + serverEnv, + defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL', + defaultApiTokenKey: '', + }); - if (settingsBaseUrl && settingsBaseUrl.length == 0) { - settingsBaseUrl = undefined; + if (!baseUrl) { + return []; } - const baseUrl = - settingsBaseUrl || - serverEnv.LMSTUDIO_API_BASE_URL || - process.env.LMSTUDIO_API_BASE_URL || - import.meta.env.LMSTUDIO_API_BASE_URL || - 'http://localhost:1234'; const response = await fetch(`${baseUrl}/v1/models`); const data = (await response.json()) as any; @@ -594,7 +656,7 @@ async function initializeModelList(options: { await Promise.all( PROVIDER_LIST.filter( (p): p is ProviderInfo & { getDynamicModels: () => Promise } => !!p.getDynamicModels, - ).map((p) => p.getDynamicModels(apiKeys, providerSettings?.[p.name], env)), + ).map((p) => p.getDynamicModels(p.name, apiKeys, providerSettings?.[p.name], env)), ) ).flat(), ...staticModels, diff --git a/vite.config.ts b/vite.config.ts index f18b8b934..d96f70451 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,7 +28,7 @@ export default defineConfig((config) => { chrome129IssuePlugin(), config.mode === 'production' && optimizeCssModules({ apply: 'build' }), ], - envPrefix: ["VITE_", "OPENAI_LIKE_API_", "OLLAMA_API_BASE_URL", "LMSTUDIO_API_BASE_URL","TOGETHER_API_BASE_URL"], + envPrefix: ["VITE_", "OPENAI_LIKE_API_", "OLLAMA_API_BASE_URL", "LMSTUDIO_API_BASE_URL","TOGETHER_API_"], css: { preprocessorOptions: { scss: {