diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index dc6aecc8f..1186638ad 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -25,38 +25,91 @@ import { ExamplePrompts } from '~/components/chat/ExamplePrompts'; // @ts-ignore TODO: Introduce proper types // eslint-disable-next-line @typescript-eslint/no-unused-vars const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => { + const [customUrl, setCustomUrl] = useState(''); + const [customModel, setCustomModel] = useState(''); + + useEffect(() => { + if (provider?.name === 'OpenAILike') { + setCustomUrl(import.meta.env.OPENAI_LIKE_API_BASE_URL || ''); + setCustomModel(model || ''); + } else if (provider?.name === 'Ollama') { + setCustomUrl(import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434'); + } + }, [provider?.name]); + return ( -
- { + setProvider(providerList.find((p: ProviderInfo) => p.name === e.target.value)); - const firstModel = [...modelList].find((m) => m.provider == e.target.value); - setModel(firstModel ? firstModel.name : ''); - }} - className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all" - > - {providerList.map((provider: ProviderInfo) => ( - - ))} - - + + {provider?.name !== 'OpenAILike' && ( + + )} +
+ {(provider?.name === 'OpenAILike' || provider?.name === 'Ollama') && ( +
+ { + setCustomUrl(e.target.value); + + if (typeof window !== 'undefined') { + if (provider?.name === 'OpenAILike') { + window.localStorage.setItem('OPENAI_LIKE_API_BASE_URL', e.target.value); + } else if (provider?.name === 'Ollama') { + window.localStorage.setItem('OLLAMA_API_BASE_URL', e.target.value || 'http://localhost:11434'); + } + } + }} + className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all" + /> + {provider?.name === 'OpenAILike' && ( + { + setCustomModel(e.target.value); + setModel(e.target.value); + }} + className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all" + /> + )} +
+ )} ); }; diff --git a/app/lib/.server/llm/model.ts b/app/lib/.server/llm/model.ts index 76a371164..a95c0e2f5 100644 --- a/app/lib/.server/llm/model.ts +++ b/app/lib/.server/llm/model.ts @@ -24,9 +24,13 @@ export function getAnthropicModel(apiKey: OptionalApiKey, model: string) { return anthropic(model); } export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) { + if (!baseURL) { + throw new Error('OpenAI Like API Base URL is required'); + } + const openai = createOpenAI({ - baseURL, - apiKey, + baseURL: baseURL.endsWith('/v1') ? baseURL : `${baseURL}/v1`, + apiKey: apiKey || 'not-needed', }); return openai(model); diff --git a/app/lib/runtime/action-runner.ts b/app/lib/runtime/action-runner.ts index 5fb984fd8..fc67a9435 100644 --- a/app/lib/runtime/action-runner.ts +++ b/app/lib/runtime/action-runner.ts @@ -81,6 +81,7 @@ export class ActionRunner { if (!action) { unreachable(`Action ${actionId} not found`); + return; } if (action.executed) { @@ -100,7 +101,6 @@ export class ActionRunner { .catch((error) => { console.error('Action failed:', error); }); - return this.#currentExecutionPromise; } async #executeAction(actionId: string, isStreaming: boolean = false) { diff --git a/app/types/model.ts b/app/types/model.ts index 32522c6f3..10555cea4 100644 --- a/app/types/model.ts +++ b/app/types/model.ts @@ -7,4 +7,5 @@ export type ProviderInfo = { getApiKeyLink?: string; labelForGetApiKey?: string; icon?: string; + description?: string; }; diff --git a/app/utils/constants.ts b/app/utils/constants.ts index fcbb3f724..38fbbdeed 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -48,6 +48,8 @@ const PROVIDER_LIST: ProviderInfo[] = [ name: 'OpenAILike', staticModels: [], getDynamicModels: getOpenAILikeModels, + getApiKeyLink: '', + labelForGetApiKey: 'API Key (Optional)', }, { name: 'Cohere', @@ -268,25 +270,24 @@ const staticModels: ModelInfo[] = PROVIDER_LIST.map((p) => p.staticModels).flat( export let MODEL_LIST: ModelInfo[] = [...staticModels]; const getOllamaBaseUrl = () => { - const defaultBaseUrl = import.meta.env.OLLAMA_API_BASE_URL || 'http://localhost:11434'; - // Check if we're in the browser if (typeof window !== 'undefined') { - // Frontend always uses localhost - return defaultBaseUrl; + // Try to get URL from localStorage first, then env, then default + return ( + window.localStorage.getItem('OLLAMA_API_BASE_URL') || + import.meta.env.OLLAMA_API_BASE_URL || + 'http://localhost:11434' + ); } // Backend: Check if we're running in Docker + const defaultBaseUrl = process.env.OLLAMA_API_BASE_URL || 'http://localhost:11434'; const isDocker = process.env.RUNNING_IN_DOCKER === 'true'; return isDocker ? defaultBaseUrl.replace('localhost', 'host.docker.internal') : defaultBaseUrl; }; async function getOllamaModels(): Promise { - //if (typeof window === 'undefined') { - //return []; - //} - try { const baseUrl = getOllamaBaseUrl(); const response = await fetch(`${baseUrl}/api/tags`); @@ -298,8 +299,8 @@ async function getOllamaModels(): Promise { provider: 'Ollama', maxTokenAllowed: 8000, })); - } catch (e) { - console.error('Error getting Ollama models:', e); + } catch (error) { + console.error('Error getting Ollama models:', error); return []; } } @@ -307,26 +308,47 @@ async function getOllamaModels(): Promise { async function getOpenAILikeModels(): Promise { try { const baseUrl = import.meta.env.OPENAI_LIKE_API_BASE_URL || ''; + const customModel = typeof window !== 'undefined' ? window.localStorage.getItem('OPENAI_LIKE_MODEL') : ''; if (!baseUrl) { return []; } const apiKey = import.meta.env.OPENAI_LIKE_API_KEY ?? ''; - const response = await fetch(`${baseUrl}/models`, { - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }); - const res = (await response.json()) as any; - return res.data.map((model: any) => ({ - name: model.id, - label: model.id, - provider: 'OpenAILike', - })); - } catch (e) { - console.error('Error getting OpenAILike models:', e); + try { + const response = await fetch(`${baseUrl}/models`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + const res = (await response.json()) as any; + + return res.data.map((model: any) => ({ + name: model.id, + label: model.id, + provider: 'OpenAILike', + maxTokenAllowed: 8000, + })); + } catch (err) { + console.error('Error getting OpenAILike models:', err); + + // If we can't fetch models, return a default model if one is set + if (customModel) { + return [ + { + name: customModel, + label: customModel, + provider: 'OpenAILike', + maxTokenAllowed: 8000, + }, + ]; + } + + return []; + } + } catch (err) { + console.error('Error getting OpenAILike models:', err); return []; } } diff --git a/app/utils/logger.ts b/app/utils/logger.ts index 1a5c932c5..9b2c31c95 100644 --- a/app/utils/logger.ts +++ b/app/utils/logger.ts @@ -11,7 +11,7 @@ interface Logger { setLevel: (level: DebugLevel) => void; } -let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info'; +let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info'; const isWorker = 'HTMLRewriter' in globalThis; const supportsColor = !isWorker;