diff --git a/Cargo.lock b/Cargo.lock
index 9709d2bcf..01300c645 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14456,6 +14456,7 @@ dependencies = [
"file",
"futures-util",
"gbnf",
+ "gguf",
"llama",
"reqwest 0.12.23",
"reqwest-streams",
diff --git a/admin/server/package.json b/admin/server/package.json
index 1dc4a89eb..0123577de 100644
--- a/admin/server/package.json
+++ b/admin/server/package.json
@@ -7,17 +7,17 @@
"start": "node dist/index.js"
},
"dependencies": {
- "@hono/node-server": "^1.19.0",
+ "@hono/node-server": "^1.19.1",
"@hono/zod-validator": "^0.7.2",
"@t3-oss/env-core": "^0.13.8",
- "better-auth": "^1.3.7",
- "hono": "^4.9.4",
+ "better-auth": "^1.3.8",
+ "hono": "^4.9.6",
"qs": "^6.14.0",
- "zod": "^4.0.17"
+ "zod": "^4.1.5"
},
"devDependencies": {
- "@types/node": "^20.19.11",
- "tsx": "^4.20.4",
+ "@types/node": "^20.19.13",
+ "tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}
diff --git a/apps/admin/package.json b/apps/admin/package.json
index 8df6a9efb..f3fa33382 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -16,45 +16,45 @@
"start": "node .output/server/index.mjs"
},
"dependencies": {
- "@ai-sdk/openai": "^2.0.19",
+ "@ai-sdk/openai": "^2.0.24",
"@cerbos/grpc": "^0.22.1",
- "@libsql/client": "^0.15.12",
- "@mantine/core": "^8.2.5",
- "@mantine/form": "^8.2.5",
- "@mantine/hooks": "^8.2.5",
- "@mantine/notifications": "^8.2.5",
+ "@libsql/client": "^0.15.14",
+ "@mantine/core": "^8.2.8",
+ "@mantine/form": "^8.2.8",
+ "@mantine/hooks": "^8.2.8",
+ "@mantine/notifications": "^8.2.8",
"@tabler/icons-react": "^3.34.1",
- "@tanstack/react-query": "^5.85.5",
- "@tanstack/react-router": "^1.131.27",
+ "@tanstack/react-query": "^5.87.1",
+ "@tanstack/react-router": "^1.131.35",
"@tanstack/react-router-with-query": "^1.130.17",
- "@tanstack/react-start": "^1.131.27",
- "ai": "^5.0.21",
- "better-auth": "^1.3.7",
+ "@tanstack/react-start": "^1.131.35",
+ "ai": "^5.0.34",
+ "better-auth": "^1.3.8",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.4",
- "drizzle-orm": "^0.44.4",
+ "drizzle-orm": "^0.44.5",
"drizzle-zod": "^0.8.3",
"lucide-react": "^0.525.0",
"mantine-form-zod-resolver": "^1.3.0",
- "openid-client": "^6.6.4",
- "posthog-js": "^1.260.2",
+ "openid-client": "^6.7.1",
+ "posthog-js": "^1.261.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-preset-mantine": "^2.1.0",
- "zod": "^4.0.17"
+ "zod": "^4.1.5"
},
"devDependencies": {
- "@tailwindcss/postcss": "^4.1.12",
- "@types/node": "^22.17.2",
- "@types/react": "^18.3.23",
+ "@tailwindcss/postcss": "^4.1.13",
+ "@types/node": "^22.18.1",
+ "@types/react": "^18.3.24",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.21",
- "dotenv": "^17.2.1",
+ "dotenv": "^17.2.2",
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
- "tailwindcss": "^4.1.12",
+ "tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index ce5ee71d1..6ca10f83c 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -34,14 +34,15 @@
"@hypr/plugin-sfx": "workspace:^",
"@hypr/plugin-task": "workspace:^",
"@hypr/plugin-template": "workspace:^",
- "@hypr/plugin-windows": "workspace:^",
"@hypr/plugin-tracing": "workspace:^",
+ "@hypr/plugin-windows": "workspace:^",
"@hypr/tiptap": "workspace:^",
"@hypr/ui": "workspace:^",
"@hypr/utils": "workspace:^",
"@lingui/core": "^5.4.1",
"@lingui/react": "^5.4.1",
- "@modelcontextprotocol/sdk": "^1.17.3",
+ "@lobehub/icons": "^2.31.0",
+ "@modelcontextprotocol/sdk": "^1.17.5",
"@mux/mux-player-react": "^3.5.3",
"@mux/mux-video": "^0.26.1",
"@radix-ui/react-dialog": "^1.1.15",
@@ -53,21 +54,21 @@
"@react-pdf/renderer": "^4.3.0",
"@remixicon/react": "^4.6.0",
"@sentry/react": "^8.55.0",
- "@tanstack/react-query": "^5.85.5",
- "@tanstack/react-query-devtools": "^5.85.5",
- "@tanstack/react-router": "^1.131.27",
- "@tanstack/react-router-devtools": "^1.131.27",
- "@tanstack/zod-adapter": "^1.131.27",
+ "@tanstack/react-query": "^5.87.1",
+ "@tanstack/react-query-devtools": "^5.87.1",
+ "@tanstack/react-router": "^1.131.35",
+ "@tanstack/react-router-devtools": "^1.131.35",
+ "@tanstack/zod-adapter": "^1.131.35",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-autostart": "^2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
- "@tauri-apps/plugin-dialog": "^2.3.3",
+ "@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-http": "^2.5.2",
"@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-os": "^2.3.1",
"@tauri-apps/plugin-process": "^2.3.0",
- "@tauri-apps/plugin-shell": "^2.3.0",
+ "@tauri-apps/plugin-shell": "^2.3.1",
"@tauri-apps/plugin-store": "^2.4.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@types/lodash-es": "^4.17.12",
@@ -79,7 +80,7 @@
"html-pdf": "^3.0.1",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.10.3",
- "jspdf": "^3.0.1",
+ "jspdf": "^3.0.2",
"lodash-es": "^4.17.21",
"lucide-react": "^0.525.0",
"motion": "^11.18.2",
@@ -98,22 +99,22 @@
"tauri-plugin-keygen-api": "github:bagindo/tauri-plugin-keygen#v2",
"tauri-plugin-sentry-api": "^0.4.1",
"tippy.js": "^6.3.7",
- "zod": "^4.0.17",
+ "zod": "^4.1.5",
"zustand": "^5.0.8"
},
"devDependencies": {
- "@eslint/js": "^9.33.0",
+ "@eslint/js": "^9.35.0",
"@lingui/babel-plugin-lingui-macro": "^5.4.1",
"@lingui/cli": "^5.4.1",
"@lingui/vite-plugin": "^5.4.1",
"@mux/mux-player": "^3.5.3",
- "@tanstack/router-plugin": "^1.131.27",
- "@tauri-apps/cli": "^2.8.1",
+ "@tanstack/router-plugin": "^1.131.35",
+ "@tauri-apps/cli": "^2.8.4",
"@types/diff": "^8.0.0",
"@types/html-pdf": "^3.0.3",
"@types/jspdf": "^2.0.0",
- "@types/node": "^22.17.2",
- "@types/react": "^18.3.23",
+ "@types/node": "^22.18.1",
+ "@types/react": "^18.3.24",
"@types/react-dom": "^18.3.7",
"@types/react-grid-layout": "^1.3.5",
"@vitejs/plugin-react": "^4.7.0",
@@ -124,14 +125,14 @@
"@wdio/spec-reporter": "^8.43.0",
"@wdio/types": "^8.41.0",
"autoprefixer": "^10.4.21",
- "eslint": "^9.33.0",
+ "eslint": "^9.35.0",
"eslint-plugin-lingui": "^0.9.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^15.15.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.2",
- "typescript-eslint": "^8.40.0",
+ "typescript-eslint": "^8.42.0",
"vite": "^5.4.19",
"vite-multiple-assets": "^2.2.5",
"vitest": "^3.2.4"
diff --git a/apps/desktop/src/components/settings/components/ai/llm-local-view.tsx b/apps/desktop/src/components/settings/components/ai/llm-local-view.tsx
index a9a775f20..ba1346ed8 100644
--- a/apps/desktop/src/components/settings/components/ai/llm-local-view.tsx
+++ b/apps/desktop/src/components/settings/components/ai/llm-local-view.tsx
@@ -1,3 +1,4 @@
+import { LmStudio } from "@lobehub/icons";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { openPath } from "@tauri-apps/plugin-opener";
import { open } from "@tauri-apps/plugin-shell";
@@ -5,7 +6,7 @@ import { CloudIcon, DownloadIcon, FolderIcon } from "lucide-react";
import { useEffect } from "react";
import { useLicense } from "@/hooks/use-license";
-import { commands as localLlmCommands, type SupportedModel } from "@hypr/plugin-local-llm";
+import { commands as localLlmCommands, type CustomModelInfo, type ModelSelection } from "@hypr/plugin-local-llm";
import { commands as windowsCommands } from "@hypr/plugin-windows";
import { Button } from "@hypr/ui/components/ui/button";
import { cn } from "@hypr/ui/lib/utils";
@@ -32,9 +33,15 @@ export function LLMLocalView({
const isPro = !!getLicense.data?.valid;
const queryClient = useQueryClient();
- const currentLLMModel = useQuery({
- queryKey: ["current-llm-model"],
- queryFn: () => localLlmCommands.getCurrentModel(),
+ const currentModelSelection = useQuery({
+ queryKey: ["current-model-selection"],
+ queryFn: () => localLlmCommands.getCurrentModelSelection(),
+ });
+
+ const customModels = useQuery({
+ queryKey: ["custom-models"],
+ queryFn: () => localLlmCommands.listCustomModels(),
+ refetchInterval: 5000,
});
const handleShowFileLocation = async () => {
@@ -42,34 +49,48 @@ export function LLMLocalView({
};
useEffect(() => {
- // Auto-select current local model when switching away from remote endpoints
- if (currentLLMModel.data && !customLLMEnabled.data) {
- setSelectedLLMModel(currentLLMModel.data);
+ if (currentModelSelection.data && !customLLMEnabled.data) {
+ const selection = currentModelSelection.data;
+ if (selection.type === "Predefined") {
+ setSelectedLLMModel(selection.content.key);
+ } else if (selection.type === "Custom") {
+ setSelectedLLMModel(`custom-${selection.content.path}`);
+ }
}
- }, [currentLLMModel.data, customLLMEnabled.data, setSelectedLLMModel]);
+ }, [currentModelSelection.data, customLLMEnabled.data, setSelectedLLMModel]);
const handleLocalModelSelection = async (model: LLMModel) => {
if (model.available && model.downloaded) {
- // Update UI state first for immediate feedback
setSelectedLLMModel(model.key);
- // Then update backend state
- await localLlmCommands.setCurrentModel(model.key as SupportedModel);
- queryClient.invalidateQueries({ queryKey: ["current-llm-model"] });
+ const selection: ModelSelection = { type: "Predefined", content: { key: model.key } };
+ await localLlmCommands.setCurrentModelSelection(selection);
+ queryClient.invalidateQueries({ queryKey: ["current-model-selection"] });
- // Disable BOTH HyprCloud and custom when selecting local
setCustomLLMEnabledMutation.mutate(false);
setHyprCloudEnabledMutation.mutate(false);
setOpenAccordion(null);
- // Restart server for local model
localLlmCommands.restartServer();
}
};
+ const handleCustomModelSelection = async (customModel: CustomModelInfo) => {
+ setSelectedLLMModel(`custom-${customModel.path}`);
+
+ const selection: ModelSelection = { type: "Custom", content: { path: customModel.path } };
+ await localLlmCommands.setCurrentModelSelection(selection);
+ queryClient.invalidateQueries({ queryKey: ["current-model-selection"] });
+
+ setCustomLLMEnabledMutation.mutate(false);
+ setHyprCloudEnabledMutation.mutate(false);
+ setOpenAccordion(null);
+
+ localLlmCommands.restartServer();
+ };
+
const handleHyprCloudSelection = () => {
setSelectedLLMModel("hyprcloud");
- // Just use the configureCustomEndpoint which handles the flags
configureCustomEndpoint({
provider: "hyprcloud",
api_base: "https://pro.hyprnote.com",
@@ -80,15 +101,12 @@ export function LLMLocalView({
};
const isHyprCloudSelected = hyprCloudEnabled.data;
-
- // Base button class to remove default styling
const buttonResetClass = "appearance-none border-0 outline-0 bg-transparent p-0 m-0 font-inherit text-left w-full";
return (
- {/* HyprCloud Option */}
- {/* Main button */}
- {/* Separator */}
@@ -166,7 +182,6 @@ export function LLMLocalView({
- {/* Local Models */}
{llmModelsState.map((model) => (
))}
+
+ {customModels.data && customModels.data.length > 0 && (
+ <>
+
+
+
+ custom GGUF models
+
+
+
+ {customModels.data.map((customModel) => {
+ const isSelected = selectedLLMModel === `custom-${customModel.path}` && !customLLMEnabled.data;
+ return (
+
+ );
+ })}
+ >
+ )}
diff --git a/apps/desktop/src/components/toast/model-download.tsx b/apps/desktop/src/components/toast/model-download.tsx
index e1d9aab7b..ca192670c 100644
--- a/apps/desktop/src/components/toast/model-download.tsx
+++ b/apps/desktop/src/components/toast/model-download.tsx
@@ -1,6 +1,7 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
+import { commands as connectorCommands } from "@hypr/plugin-connector";
import { commands as localLlmCommands } from "@hypr/plugin-local-llm";
import { commands as localSttCommands } from "@hypr/plugin-local-stt";
import { sonnerToast, toast } from "@hypr/ui/components/ui/toast";
@@ -23,6 +24,26 @@ export default function ModelDownloadNotification() {
queryFn: () => localLlmCommands.getCurrentModel(),
});
+ const sttProvider = useQuery({
+ queryKey: ["stt-provider"],
+ queryFn: () => localSttCommands.getProvider(),
+ });
+
+ const llmProviderSource = useQuery({
+ queryKey: ["llm-provider-source"],
+ queryFn: () => connectorCommands.getProviderSource(),
+ });
+
+ const hyprcloudEnabled = useQuery({
+ queryKey: ["hyprcloud-enabled"],
+ queryFn: () => connectorCommands.getHyprcloudEnabled(),
+ });
+
+ const customLlmEnabled = useQuery({
+ queryKey: ["custom-llm-enabled"],
+ queryFn: () => connectorCommands.getCustomLlmEnabled(),
+ });
+
const checkForModelDownload = useQuery({
enabled: !!currentSttModel.data,
queryKey: ["check-model-downloaded"],
@@ -60,8 +81,12 @@ export default function ModelDownloadNotification() {
});
const sttModelExists = useQuery({
- queryKey: ["stt-model-exists"],
+ queryKey: ["stt-model-exists", sttProvider.data],
queryFn: async () => {
+ if (sttProvider.data === "Custom") {
+ return true;
+ }
+
const results = await Promise.all([
localSttCommands.isModelDownloaded("QuantizedTiny"),
localSttCommands.isModelDownloaded("QuantizedTinyEn"),
@@ -75,11 +100,20 @@ export default function ModelDownloadNotification() {
return results.some(Boolean);
},
refetchInterval: 3000,
+ enabled: !!sttProvider.data,
});
const llmModelExists = useQuery({
- queryKey: ["llm-model-exists"],
+ queryKey: ["llm-model-exists", llmProviderSource.data, hyprcloudEnabled.data, customLlmEnabled.data],
queryFn: async () => {
+ if (
+ hyprcloudEnabled.data || customLlmEnabled.data || llmProviderSource.data === "openai"
+ || llmProviderSource.data === "gemini" || llmProviderSource.data === "openrouter"
+ || llmProviderSource.data === "others"
+ ) {
+ return true;
+ }
+
const results = await Promise.all([
localLlmCommands.isModelDownloaded("Llama3p2_3bQ4"),
localLlmCommands.isModelDownloaded("HyprLLM"),
@@ -89,6 +123,7 @@ export default function ModelDownloadNotification() {
return results.some(Boolean);
},
refetchInterval: 3000,
+ enabled: !!llmProviderSource.data && hyprcloudEnabled.data !== undefined && customLlmEnabled.data !== undefined,
});
useEffect(() => {
@@ -172,6 +207,10 @@ export default function ModelDownloadNotification() {
isDismissed,
sttModelExists.data,
llmModelExists.data,
+ sttProvider.data,
+ llmProviderSource.data,
+ hyprcloudEnabled.data,
+ customLlmEnabled.data,
]);
return null;
diff --git a/apps/pro/package.json b/apps/pro/package.json
index a40bc0ebb..ea562c284 100644
--- a/apps/pro/package.json
+++ b/apps/pro/package.json
@@ -7,20 +7,20 @@
"start": "node dist/index.js"
},
"dependencies": {
- "@cacheable/memory": "^1.0.0",
- "@hono/mcp": "^0.1.1",
- "@hono/node-server": "^1.19.0",
- "@modelcontextprotocol/sdk": "^1.17.3",
+ "@cacheable/memory": "^1.0.1",
+ "@hono/mcp": "^0.1.2",
+ "@hono/node-server": "^1.19.1",
+ "@modelcontextprotocol/sdk": "^1.17.5",
"@t3-oss/env-core": "^0.13.8",
- "dotenv": "^17.2.1",
- "exa-js": "^1.9.1",
- "hono": "^4.9.4",
+ "dotenv": "^17.2.2",
+ "exa-js": "^1.9.3",
+ "hono": "^4.9.6",
"hono-rate-limiter": "^0.4.2",
"zod": "^3.25.76"
},
"devDependencies": {
- "@types/node": "^20.19.11",
- "tsx": "^4.20.4",
+ "@types/node": "^20.19.13",
+ "tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}
diff --git a/crates/gguf/src/lib.rs b/crates/gguf/src/lib.rs
index 85be7ee25..ee8344f21 100644
--- a/crates/gguf/src/lib.rs
+++ b/crates/gguf/src/lib.rs
@@ -18,100 +18,50 @@ mod utils;
pub use utils::*;
pub trait GgufExt {
- fn gguf_chat_format(&self) -> Result