Unverified 提交 5fc49231 authored 作者: Piotr Wilkin (ilintar)'s avatar Piotr Wilkin (ilintar) 提交者: GitHub

Add LM Studio support (#22)

上级 35296271
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"@ai-sdk/anthropic": "^1.2.8", "@ai-sdk/anthropic": "^1.2.8",
"@ai-sdk/google": "^1.2.10", "@ai-sdk/google": "^1.2.10",
"@ai-sdk/openai": "^1.3.7", "@ai-sdk/openai": "^1.3.7",
"@ai-sdk/openai-compatible": "^0.2.13",
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@dyad-sh/supabase-management-js": "v1.0.0", "@dyad-sh/supabase-management-js": "v1.0.0",
"@monaco-editor/react": "^4.7.0-rc.0", "@monaco-editor/react": "^4.7.0-rc.0",
...@@ -104,6 +105,51 @@ ...@@ -104,6 +105,51 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@ai-sdk/openai-compatible": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-0.2.13.tgz",
"integrity": "sha512-tB+lL8Z3j0qDod/mvxwjrPhbLUHp/aQW+NvMoJaqeTtP+Vmv5qR800pncGczxn5WN0pllQm+7aIRDnm69XeSbg==",
"dev": true,
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"@ai-sdk/provider-utils": "2.2.7"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.0.0"
}
},
"node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz",
"integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==",
"dev": true,
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider-utils": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.7.tgz",
"integrity": "sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==",
"dev": true,
"dependencies": {
"@ai-sdk/provider": "1.1.3",
"nanoid": "^3.3.8",
"secure-json-parse": "^2.7.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.23.8"
}
},
"node_modules/@ai-sdk/anthropic": { "node_modules/@ai-sdk/anthropic": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.8.tgz", "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.8.tgz",
......
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
"vitest": "^3.1.1" "vitest": "^3.1.1"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai-compatible": "^0.2.13",
"@ai-sdk/anthropic": "^1.2.8", "@ai-sdk/anthropic": "^1.2.8",
"@ai-sdk/google": "^1.2.10", "@ai-sdk/google": "^1.2.10",
"@ai-sdk/openai": "^1.3.7", "@ai-sdk/openai": "^1.3.7",
......
...@@ -4,3 +4,7 @@ import { type LocalModel } from "@/ipc/ipc_types"; ...@@ -4,3 +4,7 @@ import { type LocalModel } from "@/ipc/ipc_types";
export const localModelsAtom = atom<LocalModel[]>([]); export const localModelsAtom = atom<LocalModel[]>([]);
export const localModelsLoadingAtom = atom<boolean>(false); export const localModelsLoadingAtom = atom<boolean>(false);
export const localModelsErrorAtom = atom<Error | null>(null); export const localModelsErrorAtom = atom<Error | null>(null);
export const lmStudioModelsAtom = atom<LocalModel[]>([]);
export const lmStudioModelsLoadingAtom = atom<boolean>(false);
export const lmStudioModelsErrorAtom = atom<Error | null>(null);
...@@ -8,7 +8,7 @@ export interface ModelOption { ...@@ -8,7 +8,7 @@ export interface ModelOption {
contextWindow?: number; contextWindow?: number;
} }
type RegularModelProvider = Exclude<ModelProvider, "ollama">; type RegularModelProvider = Exclude<ModelProvider, "ollama" | "lmstudio">;
export const MODEL_OPTIONS: Record<RegularModelProvider, ModelOption[]> = { export const MODEL_OPTIONS: Record<RegularModelProvider, ModelOption[]> = {
openai: [ openai: [
// https://platform.openai.com/docs/models/gpt-4.1 // https://platform.openai.com/docs/models/gpt-4.1
......
import { useCallback } from "react";
import { useAtom } from "jotai";
import {
lmStudioModelsAtom,
lmStudioModelsLoadingAtom,
lmStudioModelsErrorAtom,
} from "@/atoms/localModelsAtoms";
import { IpcClient } from "@/ipc/ipc_client";
export function useLocalLMSModels() {
const [models, setModels] = useAtom(lmStudioModelsAtom);
const [loading, setLoading] = useAtom(lmStudioModelsLoadingAtom);
const [error, setError] = useAtom(lmStudioModelsErrorAtom);
const ipcClient = IpcClient.getInstance();
/**
* Load local models from Ollama
*/
const loadModels = useCallback(async () => {
setLoading(true);
try {
const modelList = await ipcClient.listLocalLMStudioModels();
setModels(modelList);
setError(null);
return modelList;
} catch (error) {
console.error("Error loading local LMStudio models:", error);
setError(error instanceof Error ? error : new Error(String(error)));
return [];
} finally {
setLoading(false);
}
}, [ipcClient, setModels, setError, setLoading]);
return {
models,
loading,
error,
loadModels,
};
}
...@@ -20,13 +20,13 @@ export function useLocalModels() { ...@@ -20,13 +20,13 @@ export function useLocalModels() {
const loadModels = useCallback(async () => { const loadModels = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const modelList = await ipcClient.listLocalModels(); const modelList = await ipcClient.listLocalOllamaModels();
setModels(modelList); setModels(modelList);
setError(null); setError(null);
return modelList; return modelList;
} catch (error) { } catch (error) {
console.error("Error loading local models:", error); console.error("Error loading local Ollama models:", error);
setError(error instanceof Error ? error : new Error(String(error))); setError(error instanceof Error ? error : new Error(String(error)));
return []; return [];
} finally { } finally {
......
import { ipcMain } from "electron"; import { registerOllamaHandlers } from "./local_model_ollama_handler";
import log from "electron-log"; import { registerLMStudioHandlers } from "./local_model_lmstudio_handler";
import { LocalModelListResponse, LocalModel } from "../ipc_types";
const logger = log.scope("local_model_handlers");
const OLLAMA_API_URL = "http://localhost:11434";
interface OllamaModel {
name: string;
modified_at: string;
size: number;
digest: string;
details: {
format: string;
family: string;
families: string[];
parameter_size: string;
quantization_level: string;
};
}
export function registerLocalModelHandlers() { export function registerLocalModelHandlers() {
// Get list of models from Ollama registerOllamaHandlers();
ipcMain.handle( registerLMStudioHandlers();
"local-models:list",
async (): Promise<LocalModelListResponse> => {
try {
const response = await fetch(`${OLLAMA_API_URL}/api/tags`);
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.statusText}`);
}
const data = await response.json();
const ollamaModels: OllamaModel[] = data.models || [];
// Transform the data to return just what we need
const models: LocalModel[] = ollamaModels.map((model) => {
// Extract display name by cleaning up the model name
// For names like "llama2:latest" we want to show "Llama 2"
let displayName = model.name.split(":")[0]; // Remove tags like ":latest"
// Capitalize and add spaces for readability
displayName = displayName
.replace(/-/g, " ")
.replace(/(\d+)/, " $1 ") // Add spaces around numbers
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
.trim();
return {
modelName: model.name, // The actual model name used for API calls
displayName, // The user-friendly name
};
});
logger.info(
`Successfully fetched ${models.length} local models from Ollama`
);
return { models, error: null };
} catch (error) {
if (
error instanceof TypeError &&
(error as Error).message.includes("fetch failed")
) {
logger.error("Could not connect to Ollama. Is it running?");
return {
models: [],
error:
"Could not connect to Ollama. Make sure it's running at http://localhost:11434",
};
}
logger.error("Error fetching local models:", error);
return { models: [], error: "Failed to fetch models from Ollama" };
}
}
);
} }
import { ipcMain } from "electron";
import log from "electron-log";
import type { LocalModelListResponse, LocalModel } from "../ipc_types";
const logger = log.scope("lmstudio_handler");
export interface LMStudioModel {
type: "llm" | "embedding" | string;
id: string;
object: string;
publisher: string;
state: "loaded" | "not-loaded";
max_context_length: number;
quantization: string
compatibility_type: string
arch: string;
[key: string]: any;
}
export async function fetchLMStudioModels(): Promise<LocalModelListResponse> {
try {
const modelsResponse: Response = await fetch("http://localhost:1234/api/v0/models");
if (!modelsResponse.ok) {
throw new Error("Failed to fetch models from LM Studio");
}
const modelsJson = await modelsResponse.json();
const downloadedModels = modelsJson.data as LMStudioModel[];
const models: LocalModel[] = downloadedModels
.filter((model: any) => model.type === "llm")
.map((model: any) => ({
modelName: model.id,
displayName: model.id,
provider: "lmstudio"
}));
logger.info(`Successfully fetched ${models.length} models from LM Studio`);
return { models, error: null };
} catch (error) {
return { models: [], error: "Failed to fetch models from LM Studio" };
}
}
export function registerLMStudioHandlers() {
ipcMain.handle('local-models:list-lmstudio', async (): Promise<LocalModelListResponse> => {
return fetchLMStudioModels();
});
}
\ No newline at end of file
import { ipcMain } from "electron";
import log from "electron-log";
import { LocalModelListResponse, LocalModel } from "../ipc_types";
const logger = log.scope("ollama_handler");
const OLLAMA_API_URL = "http://localhost:11434";
interface OllamaModel {
name: string;
modified_at: string;
size: number;
digest: string;
details: {
format: string;
family: string;
families: string[];
parameter_size: string;
quantization_level: string;
};
}
export async function fetchOllamaModels(): Promise<LocalModelListResponse> {
try {
const response = await fetch(`${OLLAMA_API_URL}/api/tags`);
if (!response.ok) {
throw new Error(`Failed to fetch model: ${response.statusText}`);
}
const data = await response.json();
const ollamaModels: OllamaModel[] = data.models || [];
const models: LocalModel[] = ollamaModels.map((model: OllamaModel) => {
const displayName = model.name.split(':')[0]
.replace(/-/g, ' ')
.replace(/(\d+)/, ' $1 ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
.trim();
return {
modelName: model.name,
displayName,
provider: "ollama",
};
});
logger.info(`Successfully fetched ${models.length} models from Ollama`);
return { models, error: null };
} catch (error) {
if (error instanceof TypeError && (error as Error).message.includes('fetch failed')) {
logger.error("Could not connect to Ollama");
return {
models: [],
error: "Could not connect to Ollama. Make sure it's running at http://localhost:11434"
};
}
return { models: [], error: "Failed to fetch models from Ollama" };
}
}
export function registerOllamaHandlers() {
ipcMain.handle('local-models:list-ollama', async (): Promise<LocalModelListResponse> => {
return fetchOllamaModels();
});
}
\ No newline at end of file
...@@ -785,14 +785,28 @@ export class IpcClient { ...@@ -785,14 +785,28 @@ export class IpcClient {
} }
} }
public async listLocalModels(): Promise<LocalModel[]> { public async listLocalOllamaModels(): Promise<LocalModel[]> {
const { models, error } = (await this.ipcRenderer.invoke( try {
"local-models:list" const response = await this.ipcRenderer.invoke("local-models:list-ollama");
)) as LocalModelListResponse; return response?.models || [];
if (error) { } catch (error) {
throw new Error(error); if (error instanceof Error) {
} throw new Error(`Failed to fetch Ollama models: ${error.message}`);
return models; }
throw new Error('Failed to fetch Ollama models: Unknown error occurred');
}
}
public async listLocalLMStudioModels(): Promise<LocalModel[]> {
try {
const response = await this.ipcRenderer.invoke("local-models:list-lmstudio");
return response?.models || [];
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch LM Studio models: ${error.message}`);
}
throw new Error('Failed to fetch LM Studio models: Unknown error occurred');
}
} }
// Listen for deep link events // Listen for deep link events
......
...@@ -94,6 +94,7 @@ export interface SystemDebugInfo { ...@@ -94,6 +94,7 @@ export interface SystemDebugInfo {
} }
export interface LocalModel { export interface LocalModel {
provider: "ollama" | "lmstudio";
modelName: string; // Name used for API calls (e.g., "llama2:latest") modelName: string; // Name used for API calls (e.g., "llama2:latest")
displayName: string; // User-friendly name (e.g., "Llama 2") displayName: string; // User-friendly name (e.g., "Llama 2")
} }
......
...@@ -3,7 +3,7 @@ import { createGoogleGenerativeAI as createGoogle } from "@ai-sdk/google"; ...@@ -3,7 +3,7 @@ import { createGoogleGenerativeAI as createGoogle } from "@ai-sdk/google";
import { createAnthropic } from "@ai-sdk/anthropic"; import { createAnthropic } from "@ai-sdk/anthropic";
import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { createOllama } from "ollama-ai-provider"; import { createOllama } from "ollama-ai-provider";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import type { LargeLanguageModel, UserSettings } from "../../lib/schemas"; import type { LargeLanguageModel, UserSettings } from "../../lib/schemas";
import { import {
PROVIDER_TO_ENV_VAR, PROVIDER_TO_ENV_VAR,
...@@ -83,6 +83,12 @@ export function getModelClient( ...@@ -83,6 +83,12 @@ export function getModelClient(
const provider = createOllama(); const provider = createOllama();
return provider(model.name); return provider(model.name);
} }
case "lmstudio": {
// Using LM Studio's OpenAI compatible API
const baseURL = "http://localhost:1234/v1"; // Default LM Studio OpenAI API URL
const provider = createOpenAICompatible({ name: "lmstudio", baseURL });
return provider(model.name);
}
default: { default: {
// Ensure exhaustive check if more providers are added // Ensure exhaustive check if more providers are added
const _exhaustiveCheck: never = model.provider; const _exhaustiveCheck: never = model.provider;
......
...@@ -36,6 +36,7 @@ export const ModelProviderSchema = z.enum([ ...@@ -36,6 +36,7 @@ export const ModelProviderSchema = z.enum([
"auto", "auto",
"openrouter", "openrouter",
"ollama", "ollama",
"lmstudio",
]); ]);
/** /**
......
...@@ -49,7 +49,8 @@ const validInvokeChannels = [ ...@@ -49,7 +49,8 @@ const validInvokeChannels = [
"supabase:list-projects", "supabase:list-projects",
"supabase:set-app-project", "supabase:set-app-project",
"supabase:unset-app-project", "supabase:unset-app-project",
"local-models:list", "local-models:list-ollama",
"local-models:list-lmstudio",
"window:minimize", "window:minimize",
"window:maximize", "window:maximize",
"window:close", "window:close",
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论