diff --git a/README.md b/README.md index 1ade0b9a..c055797b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai - ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more - 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others - - 💻 Local Model Support with Ollama + - 💻 Local Model Support with Ollama, LM Studio 2. **AI Assistants & Conversations**: diff --git a/docs/README.ja.md b/docs/README.ja.md index e38bdbff..9ba1c69f 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -31,7 +31,7 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク - ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など - 🔗 AI Web サービス統合:Claude、Peplexity、Poe など - - 💻 Ollama によるローカルモデル実行対応 + - 💻 Ollama、LM Studio によるローカルモデル実行対応 2. **AI アシスタントと対話**: diff --git a/docs/README.zh.md b/docs/README.zh.md index 40da0a77..730b1e93 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -31,7 +31,7 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客 - ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等 - 🔗 集成流行 AI Web 服务:Claude、Peplexity、Poe、腾讯元宝、知乎直答等 - - 💻 支持 Ollama 本地模型部署 + - 💻 支持 Ollama、LM Studio 本地模型部署 2. **智能助手与对话**: diff --git a/src/renderer/src/assets/images/providers/lmstudio.png b/src/renderer/src/assets/images/providers/lmstudio.png new file mode 100644 index 00000000..6f5fc077 Binary files /dev/null and b/src/renderer/src/assets/images/providers/lmstudio.png differ diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 3bc3870c..fdebee5b 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -323,6 +323,7 @@ export const SYSTEM_MODELS: Record = { } ], ollama: [], + lmstudio: [], silicon: [ { id: 'deepseek-ai/DeepSeek-R1', diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 9fa59ae7..28392e52 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -24,6 +24,7 @@ import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.png import NvidiaProviderLogo from '@renderer/assets/images/providers/nvidia.png' import OcoolAiProviderLogo from '@renderer/assets/images/providers/ocoolai.png' import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png' +import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png' import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png' import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png' import PerplexityProviderLogo from '@renderer/assets/images/providers/perplexity.png' @@ -52,6 +53,8 @@ export function getProviderLogo(providerId: string) { return ZhipuProviderLogo case 'ollama': return OllamaProviderLogo + case 'lmstudio': + return LMStudioProviderLogo case 'moonshot': return MoonshotProviderLogo case 'openrouter': @@ -373,6 +376,16 @@ export const PROVIDER_CONFIG = { models: 'https://ollama.com/library' } }, + lmstudio: { + api: { + url: 'http://localhost:1234' + }, + websites: { + official: 'https://lmstudio.ai/', + docs: 'https://lmstudio.ai/docs', + models: 'https://lmstudio.ai/models' + } + }, anthropic: { api: { url: 'https://api.anthropic.com/' diff --git a/src/renderer/src/hooks/useLMStudio.ts b/src/renderer/src/hooks/useLMStudio.ts new file mode 100644 index 00000000..687ed4ed --- /dev/null +++ b/src/renderer/src/hooks/useLMStudio.ts @@ -0,0 +1,18 @@ +import store, { useAppSelector } from '@renderer/store' +import { setLMStudioKeepAliveTime } from '@renderer/store/llm' +import { useDispatch } from 'react-redux' + +export function useLMStudioSettings() { + const settings = useAppSelector((state) => state.llm.settings.lmstudio) + const dispatch = useDispatch() + + return { ...settings, setKeepAliveTime: (time: number) => dispatch(setLMStudioKeepAliveTime(time)) } +} + +export function getLMStudioSettings() { + return store.getState().llm.settings.lmstudio +} + +export function getLMStudioKeepAliveTime() { + return store.getState().llm.settings.lmstudio.keepAliveTime + 'm' +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 24b723c6..30d3a06a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -439,6 +439,12 @@ "keep_alive_time.title": "Keep Alive Time", "title": "Ollama" }, + "lmstudio": { + "keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.", + "keep_alive_time.placeholder": "Minutes", + "keep_alive_time.title": "Keep Alive Time", + "title": "LM Studio" + }, "paintings": { "button.delete.image": "Delete Image", "button.delete.image.confirm": "Are you sure you want to delete this image?", @@ -493,6 +499,7 @@ "nvidia": "Nvidia", "ocoolai": "ocoolAI", "ollama": "Ollama", + "lmstudio": "LM Studio", "openai": "OpenAI", "openrouter": "OpenRouter", "ppio": "PPIO", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 50f4bf89..243cbe77 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -439,6 +439,12 @@ "keep_alive_time.title": "保持時間", "title": "Ollama" }, + "lmstudio": { + "keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)", + "keep_alive_time.placeholder": "分", + "keep_alive_time.title": "保持時間", + "title": "LM Studio" + }, "paintings": { "button.delete.image": "画像を削除", "button.delete.image.confirm": "この画像を削除してもよろしいですか?", @@ -493,6 +499,7 @@ "nvidia": "NVIDIA", "ocoolai": "ocoolAI", "ollama": "Ollama", + "lmstudio": "LM Studio", "openai": "OpenAI", "openrouter": "OpenRouter", "qwenlm": "QwenLM", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 110aee9c..f83a3599 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -439,6 +439,12 @@ "keep_alive_time.title": "Время жизни модели", "title": "Ollama" }, + "lmstudio": { + "keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.", + "keep_alive_time.placeholder": "Минуты", + "keep_alive_time.title": "Время жизни модели", + "title": "LM Studio" + }, "paintings": { "button.delete.image": "Удалить изображение", "button.delete.image.confirm": "Вы уверены, что хотите удалить это изображение?", @@ -493,6 +499,7 @@ "nvidia": "Nvidia", "ocoolai": "ocoolAI", "ollama": "Ollama", + "lmstudio": "LM Studio", "openai": "OpenAI", "openrouter": "OpenRouter", "qwenlm": "QwenLM", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 1e844b96..e5f7b564 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -439,6 +439,12 @@ "keep_alive_time.title": "保持活跃时间", "title": "Ollama" }, + "lmstudio": { + "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)", + "keep_alive_time.placeholder": "分钟", + "keep_alive_time.title": "保持活跃时间", + "title": "LM Studio" + }, "paintings": { "button.delete.image": "删除图片", "button.delete.image.confirm": "确定要删除此图片吗?", @@ -493,6 +499,7 @@ "nvidia": "英伟达", "ocoolai": "ocoolAI", "ollama": "Ollama", + "lmstudio": "LM Studio", "openai": "OpenAI", "openrouter": "OpenRouter", "ppio": "PPIO 派欧云", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index bacf5fcc..5577bd0f 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -439,6 +439,12 @@ "keep_alive_time.title": "保持活躍時間", "title": "Ollama" }, + "lmstudio": { + "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。", + "keep_alive_time.placeholder": "分鐘", + "keep_alive_time.title": "保持活躍時間", + "title": "LM Studio" + }, "paintings": { "infini": "無問芯穹", "perplexity": "Perplexity", @@ -493,6 +499,7 @@ "nvidia": "輝達", "ocoolai": "ocoolAI", "ollama": "Ollama", + "lmstudio": "LM Studio", "openai": "OpenAI", "openrouter": "OpenRouter", "ppio": "PPIO 派歐雲", diff --git a/src/renderer/src/pages/settings/ProviderSettings/LMStudioSettings.tsx b/src/renderer/src/pages/settings/ProviderSettings/LMStudioSettings.tsx new file mode 100644 index 00000000..9b7adf2b --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/LMStudioSettings.tsx @@ -0,0 +1,34 @@ +import { useLMStudioSettings } from '@renderer/hooks/useLMStudio' +import { InputNumber } from 'antd' +import { FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..' + +const LMStudioSettings: FC = () => { + const { keepAliveTime, setKeepAliveTime } = useLMStudioSettings() + const [keepAliveMinutes, setKeepAliveMinutes] = useState(keepAliveTime) + const { t } = useTranslation() + + return ( + + {t('lmstudio.keep_alive_time.title')} + setKeepAliveMinutes(Number(e))} + onBlur={() => setKeepAliveTime(keepAliveMinutes)} + suffix={t('lmstudio.keep_alive_time.placeholder')} + step={5} + /> + + {t('lmstudio.keep_alive_time.description')} + + + ) +} + +const Container = styled.div`` + +export default LMStudioSettings diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 1ca7a2d9..af03369b 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -43,6 +43,7 @@ import ApiCheckPopup from './ApiCheckPopup' import EditModelsPopup from './EditModelsPopup' import GraphRAGSettings from './GraphRAGSettings' import OllamSettings from './OllamaSettings' +import LMStudioSettings from './LMStudioSettings' import SelectProviderModelPopup from './SelectProviderModelPopup' interface Props { @@ -319,6 +320,7 @@ const ProviderSetting: FC = ({ provider: _provider }) => { )} {provider.id === 'ollama' && } + {provider.id === 'lmstudio' && } {provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && ( )} diff --git a/src/renderer/src/providers/BaseProvider.ts b/src/renderer/src/providers/BaseProvider.ts index e359e376..54128898 100644 --- a/src/renderer/src/providers/BaseProvider.ts +++ b/src/renderer/src/providers/BaseProvider.ts @@ -1,5 +1,6 @@ import { REFERENCE_PROMPT } from '@renderer/config/prompts' import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama' +import { getLMStudioKeepAliveTime } from '@renderer/hooks/useLMStudio' import { getKnowledgeReferences } from '@renderer/services/KnowledgeService' import store from '@renderer/store' import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types' @@ -63,7 +64,7 @@ export default abstract class BaseProvider { } public get keepAliveTime() { - return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined + return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : this.provider.id === 'lmstudio' ? getLMStudioKeepAliveTime() : undefined } public async fakeCompletions({ onChunk }: CompletionsParams) { diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index d4378923..7700d540 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -214,7 +214,7 @@ export async function checkApi(provider: Provider, model: Model) { const key = 'api-check' const style = { marginTop: '3vh' } - if (provider.id !== 'ollama') { + if (provider.id !== 'ollama' && provider.id !== 'lmstudio') { if (!provider.apiKey) { window.message.error({ content: i18n.t('message.error.enter.api.key'), key, style }) return { @@ -252,7 +252,7 @@ export async function checkApi(provider: Provider, model: Model) { function hasApiKey(provider: Provider) { if (!provider) return false - if (provider.id === 'ollama') return true + if (provider.id === 'ollama' || provider.id === 'lmstudio') return true return !isEmpty(provider.apiKey) } diff --git a/src/renderer/src/store/llm.ts b/src/renderer/src/store/llm.ts index cec928cc..0fa68195 100644 --- a/src/renderer/src/store/llm.ts +++ b/src/renderer/src/store/llm.ts @@ -8,6 +8,9 @@ type LlmSettings = { ollama: { keepAliveTime: number } + lmstudio: { + keepAliveTime: number + } } export interface LlmState { @@ -83,6 +86,16 @@ const initialState: LlmState = { isSystem: true, enabled: false }, + { + id: 'lmstudio', + name: 'LM Studio', + type: 'openai', + apiKey: '', + apiHost: 'http://localhost:1234', + models: SYSTEM_MODELS.lmstudio, + isSystem: true, + enabled: false + }, { id: 'anthropic', name: 'Anthropic', @@ -378,6 +391,9 @@ const initialState: LlmState = { settings: { ollama: { keepAliveTime: 0 + }, + lmstudio: { + keepAliveTime: 0 } } } @@ -398,11 +414,24 @@ const getIntegratedInitialState = () => { models: [model], isSystem: true, enabled: true + }, + { + id: 'lmstudio', + name: 'LM Studio', + type: 'openai', + apiKey: '', + apiHost: 'http://localhost:1234', + models: [model], + isSystem: true, + enabled: true } ], settings: { ollama: { keepAliveTime: 3600 + }, + lmstudio: { + keepAliveTime: 3600 } } } as LlmState @@ -457,6 +486,9 @@ const settingsSlice = createSlice({ }, setOllamaKeepAliveTime: (state, action: PayloadAction) => { state.settings.ollama.keepAliveTime = action.payload + }, + setLMStudioKeepAliveTime: (state, action: PayloadAction) => { + state.settings.lmstudio.keepAliveTime = action.payload } } }) @@ -471,7 +503,8 @@ export const { setDefaultModel, setTopicNamingModel, setTranslateModel, - setOllamaKeepAliveTime + setOllamaKeepAliveTime, + setLMStudioKeepAliveTime } = settingsSlice.actions export default settingsSlice.reducer diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index e7e06555..f43bf3a6 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -970,6 +970,16 @@ const migrateConfig = { } state.llm.providers.push( + { + id: 'lmstudio', + name: 'LM Studio', + type: 'openai', + apiKey: '', + apiHost: 'http://localhost:1234', + models: SYSTEM_MODELS.lmstudio, + isSystem: true, + enabled: false + }, { id: 'perplexity', name: 'Perplexity', @@ -1001,6 +1011,11 @@ const migrateConfig = { enabled: false } ) + + state.llm.settings.lmstudio = { + keepAliveTime: 5 + } + return state } } diff --git a/yarn.lock b/yarn.lock index 21f845a7..36bc3c83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8330,6 +8330,7 @@ __metadata: "@langchain/google-vertexai": "*" "@langchain/google-vertexai-web": "*" "@langchain/groq": "*" + "@langchain/lmstudio": "*" "@langchain/mistralai": "*" "@langchain/ollama": "*" axios: "*" @@ -8356,6 +8357,8 @@ __metadata: optional: true "@langchain/groq": optional: true + "@langchain/lmstudio": + optional: true "@langchain/mistralai": optional: true "@langchain/ollama":