diff --git a/src/lib/models/providers/index.ts b/src/lib/models/providers/index.ts index 2c7ef2c..b3dc550 100644 --- a/src/lib/models/providers/index.ts +++ b/src/lib/models/providers/index.ts @@ -5,6 +5,7 @@ import OllamaProvider from './ollama'; import GeminiProvider from './gemini'; import TransformersProvider from './transformers'; import GroqProvider from './groq'; +import LemonadeProvider from './lemonade'; export const providers: Record> = { openai: OpenAIProvider, @@ -12,6 +13,7 @@ export const providers: Record> = { gemini: GeminiProvider, transformers: TransformersProvider, groq: GroqProvider, + lemonade: LemonadeProvider }; export const getModelProvidersUIConfigSection = diff --git a/src/lib/models/providers/lemonade/index.ts b/src/lib/models/providers/lemonade/index.ts new file mode 100644 index 0000000..582363a --- /dev/null +++ b/src/lib/models/providers/lemonade/index.ts @@ -0,0 +1,152 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; +import { Embeddings } from '@langchain/core/embeddings'; +import { UIConfigField } from '@/lib/config/types'; +import { getConfiguredModelProviderById } from '@/lib/config/serverRegistry'; +import BaseModelProvider from '../../base/provider'; +import { Model, ModelList, ProviderMetadata } from '../../types'; +import BaseLLM from '../../base/llm'; +import LemonadeLLM from './lemonadeLLM'; +import BaseEmbedding from '../../base/embedding'; +import LemonadeEmbedding from './lemonadeEmbedding'; + +interface LemonadeConfig { + baseURL: string; + apiKey?: string; +} + +const providerConfigFields: UIConfigField[] = [ + { + type: 'string', + name: 'Base URL', + key: 'baseURL', + description: 'The base URL for Lemonade API', + required: true, + placeholder: 'https://api.lemonade.ai/v1', + env: 'LEMONADE_BASE_URL', + scope: 'server', + }, + { + type: 'password', + name: 'API Key', + key: 'apiKey', + description: 'Your Lemonade API key (optional)', + required: false, + placeholder: 'Lemonade API Key', + env: 'LEMONADE_API_KEY', + scope: 'server', + }, +]; + +class LemonadeProvider extends BaseModelProvider { + constructor(id: string, name: string, config: LemonadeConfig) { + super(id, name, config); + } + + async getDefaultModels(): Promise { + try { + const res = await fetch(`${this.config.baseURL}/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...this.config.apiKey ? {'Authorization': `Bearer ${this.config.apiKey}`} : {} + }, + }); + + const data = await res.json(); + + const models: Model[] = data.data.filter((m: any) => m.recipe === 'llamacpp').map((m: any) => { + return { + name: m.id, + key: m.id, + }; + }); + + return { + embedding: models, + chat: models, + }; + } catch (err) { + if (err instanceof TypeError) { + throw new Error( + 'Error connecting to Lemonade API. Please ensure the base URL is correct and the service is available.', + ); + } + + throw err; + } + } + + async getModelList(): Promise { + const defaultModels = await this.getDefaultModels(); + const configProvider = getConfiguredModelProviderById(this.id)!; + + return { + embedding: [ + ...defaultModels.embedding, + ...configProvider.embeddingModels, + ], + chat: [...defaultModels.chat, ...configProvider.chatModels], + }; + } + + async loadChatModel(key: string): Promise> { + const modelList = await this.getModelList(); + + const exists = modelList.chat.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Lemonade Chat Model. Invalid Model Selected', + ); + } + + return new LemonadeLLM({ + apiKey: this.config.apiKey || 'not-needed', + model: key, + baseURL: this.config.baseURL, + }); + } + + async loadEmbeddingModel(key: string): Promise> { + const modelList = await this.getModelList(); + const exists = modelList.embedding.find((m) => m.key === key); + + if (!exists) { + throw new Error( + 'Error Loading Lemonade Embedding Model. Invalid Model Selected.', + ); + } + + return new LemonadeEmbedding({ + apiKey: this.config.apiKey || 'not-needed', + model: key, + baseURL: this.config.baseURL, + }); + } + + static parseAndValidate(raw: any): LemonadeConfig { + if (!raw || typeof raw !== 'object') + throw new Error('Invalid config provided. Expected object'); + if (!raw.baseURL) + throw new Error('Invalid config provided. Base URL must be provided'); + + return { + baseURL: String(raw.baseURL), + apiKey: raw.apiKey ? String(raw.apiKey) : undefined, + }; + } + + static getProviderConfigFields(): UIConfigField[] { + return providerConfigFields; + } + + static getProviderMetadata(): ProviderMetadata { + return { + key: 'lemonade', + name: 'Lemonade', + }; + } +} + +export default LemonadeProvider; \ No newline at end of file diff --git a/src/lib/models/providers/lemonade/lemonadeEmbedding.ts b/src/lib/models/providers/lemonade/lemonadeEmbedding.ts new file mode 100644 index 0000000..a8e2155 --- /dev/null +++ b/src/lib/models/providers/lemonade/lemonadeEmbedding.ts @@ -0,0 +1,5 @@ +import OpenAIEmbedding from "../openai/openaiEmbedding"; + +class LemonadeEmbedding extends OpenAIEmbedding {} + +export default LemonadeEmbedding; \ No newline at end of file diff --git a/src/lib/models/providers/lemonade/lemonadeLLM.ts b/src/lib/models/providers/lemonade/lemonadeLLM.ts new file mode 100644 index 0000000..d207e13 --- /dev/null +++ b/src/lib/models/providers/lemonade/lemonadeLLM.ts @@ -0,0 +1,5 @@ +import OpenAILLM from "../openai/openaiLLM"; + +class LemonadeLLM extends OpenAILLM {} + +export default LemonadeLLM; \ No newline at end of file