diff --git a/apps/desktop/src/components/settings/ai/llm/configure.tsx b/apps/desktop/src/components/settings/ai/llm/configure.tsx
index 6f85a860d..6ed456ac2 100644
--- a/apps/desktop/src/components/settings/ai/llm/configure.tsx
+++ b/apps/desktop/src/components/settings/ai/llm/configure.tsx
@@ -16,10 +16,7 @@ export function ConfigureProviders() {
{PROVIDERS.map((provider) => (
))}
@@ -27,25 +24,15 @@ export function ConfigureProviders() {
);
}
-function ProviderCard({
- providerId,
- providerName,
- icon,
- providerConfig,
-}: {
- providerId: ProviderId;
- providerName: string;
- icon: React.ReactNode;
- providerConfig: typeof PROVIDERS[number];
-}) {
- const [provider, setProvider] = useProvider(providerId);
+function ProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
+ const [provider, setProvider] = useProvider(config.id);
const form = useForm({
onSubmit: ({ value }) => setProvider(value),
defaultValues: provider
?? ({
type: "llm",
- base_url: "",
+ base_url: config.baseUrl ?? "",
api_key: "",
} satisfies internal.AIProvider),
listeners: {
@@ -65,22 +52,22 @@ function ProviderCard({
return (
- {icon}
- {providerName}
- {providerConfig.badge && (
+ {config.icon}
+ {config.displayName}
+ {config.badge && (
- {providerConfig.badge}
+ {config.badge}
)}
-
+
- {(field) => (
-
- )}
-
-
- {(field) => (
-
- )}
-
+ {!config.baseUrl && (
+
+ {(field) => (
+
+ )}
+
+ )}
+ {config?.apiKey && (
+
+ {(field) => (
+
+ )}
+
+ )}
+ {config.baseUrl && (
+
+
+ Advanced
+
+
+
+ {(field) => (
+
+ )}
+
+
+
+ )}
@@ -119,6 +126,8 @@ function ProviderCard({
function ProviderContext({ providerId }: { providerId: ProviderId }) {
const content = providerId === "hyprnote"
? "Hyprnote is great"
+ : providerId === "custom"
+ ? "We only support **OpenAI compatible** endpoints for now."
: "";
if (!content) {
diff --git a/apps/desktop/src/components/settings/ai/llm/shared.tsx b/apps/desktop/src/components/settings/ai/llm/shared.tsx
index 0d80d60e3..1b090e13d 100644
--- a/apps/desktop/src/components/settings/ai/llm/shared.tsx
+++ b/apps/desktop/src/components/settings/ai/llm/shared.tsx
@@ -1,3 +1,4 @@
+import { Icon } from "@iconify-icon/react";
import { Anthropic, LmStudio, Ollama, OpenAI, OpenRouter } from "@lobehub/icons";
export type ProviderId = typeof PROVIDERS[number]["id"];
@@ -9,7 +10,7 @@ export const PROVIDERS = [
badge: "Recommended",
icon:
,
apiKey: false,
- baseUrl: { value: "https://api.hyprnote.com/v1", immutable: true },
+ baseUrl: "https://api.hyprnote.com/v1",
},
{
id: "openai",
@@ -17,7 +18,7 @@ export const PROVIDERS = [
badge: null,
icon: ,
apiKey: true,
- baseUrl: { value: "https://api.openai.com/v1", immutable: true },
+ baseUrl: "https://api.openai.com/v1",
},
{
id: "anthropic",
@@ -25,7 +26,7 @@ export const PROVIDERS = [
badge: null,
icon: ,
apiKey: true,
- baseUrl: { value: "https://api.anthropic.com/v1", immutable: true },
+ baseUrl: "https://api.anthropic.com/v1",
},
{
id: "openrouter",
@@ -33,7 +34,7 @@ export const PROVIDERS = [
badge: null,
icon: ,
apiKey: true,
- baseUrl: { value: "https://openrouter.ai/api/v1", immutable: true },
+ baseUrl: "https://openrouter.ai/api/v1",
},
{
id: "ollama",
@@ -41,7 +42,7 @@ export const PROVIDERS = [
badge: null,
icon: ,
apiKey: false,
- baseUrl: { value: "http://localhost:11434", immutable: false },
+ baseUrl: "http://localhost:11434",
},
{
id: "lmstudio",
@@ -49,6 +50,14 @@ export const PROVIDERS = [
badge: null,
icon: ,
apiKey: false,
- baseUrl: { value: "http://localhost:8000", immutable: false },
+ baseUrl: "http://localhost:8000",
+ },
+ {
+ id: "custom",
+ displayName: "Custom",
+ badge: null,
+ icon: ,
+ apiKey: true,
+ baseUrl: undefined,
},
] as const;
diff --git a/apps/desktop/src/components/settings/ai/shared/index.tsx b/apps/desktop/src/components/settings/ai/shared/index.tsx
index eb0edec35..90d32880f 100644
--- a/apps/desktop/src/components/settings/ai/shared/index.tsx
+++ b/apps/desktop/src/components/settings/ai/shared/index.tsx
@@ -26,23 +26,25 @@ export function FormField({
icon,
placeholder,
type,
- hidden,
}: {
field: AnyFieldApi;
label: string;
icon: string;
placeholder?: string;
type?: string;
- hidden?: boolean;
}) {
- const { meta: { errors, isTouched, isDirty } } = field.state;
- const hasError = isDirty && isTouched && errors && errors.length > 0;
+ const { meta: { errors, isTouched } } = field.state;
+ const hasError = isTouched && errors && errors.length > 0;
const errorMessage = hasError
- ? (typeof errors[0] === "string" ? errors[0] : (errors[0] as any)?.message || "Invalid value")
+ ? (typeof errors[0] === "string"
+ ? errors[0]
+ : "message" in errors[0]
+ ? errors[0].message
+ : JSON.stringify(errors[0]))
: null;
return (
-
+
diff --git a/apps/desktop/src/components/settings/ai/stt/configure.tsx b/apps/desktop/src/components/settings/ai/stt/configure.tsx
index 7bcf793f5..68643f7d8 100644
--- a/apps/desktop/src/components/settings/ai/stt/configure.tsx
+++ b/apps/desktop/src/components/settings/ai/stt/configure.tsx
@@ -52,7 +52,7 @@ function NonHyprProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
defaultValues: provider
?? ({
type: "stt",
- base_url: "",
+ base_url: config.baseUrl ?? "",
api_key: "",
} satisfies internal.AIProvider),
listeners: {
@@ -96,16 +96,17 @@ function NonHyprProviderCard({ config }: { config: typeof PROVIDERS[number] }) {
e.stopPropagation();
}}
>
-
- {(field) => (
-
- )}
-
+ {!config.baseUrl && (
+
+ {(field) => (
+
+ )}
+
+ )}
{(field) => (
)}
+ {config.baseUrl && (
+
+
+ Advanced
+
+
+
+ {(field) => (
+
+ )}
+
+
+
+ )}
@@ -370,9 +389,11 @@ function ProviderContext({ providerId }: { providerId: ProviderId }) {
? "Hyprnote curates list of on-device models and also cloud models with high-availability and performance."
: providerId === "deepgram"
? `Use [Deepgram](https://deepgram.com) for transcriptions. \
- You can choose to use the [EU Endpoint](https://developers.deepgram.com/reference/custom-endpoints#eu-endpoints) if you prefer.`
- : providerId === "deepgram-custom"
- ? `If you're using a [Dedicated endpoint](https://developers.deepgram.com/reference/custom-endpoints#deepgram-dedicated-endpoints), or other Deepgram-compatible endpoint, you can configure it here.`
+ If you want to use a [Dedicated](https://developers.deepgram.com/reference/custom-endpoints#deepgram-dedicated-endpoints)
+ or [EU](https://developers.deepgram.com/reference/custom-endpoints#eu-endpoints) endpoint,
+ you can do that in the **advanced** section.`
+ : providerId === "custom"
+ ? `We only support **Deepgram compatible** endpoints for now.`
: "";
if (!content.trim()) {
diff --git a/apps/desktop/src/components/settings/ai/stt/shared.tsx b/apps/desktop/src/components/settings/ai/stt/shared.tsx
index 9c1af3124..3242b9c03 100644
--- a/apps/desktop/src/components/settings/ai/stt/shared.tsx
+++ b/apps/desktop/src/components/settings/ai/stt/shared.tsx
@@ -27,7 +27,7 @@ export const PROVIDERS = [
id: "hyprnote",
displayName: "Hyprnote",
icon:
,
- baseUrl: { value: "https://api.hyprnote.com/v1", immutable: true },
+ baseUrl: "https://api.hyprnote.com/v1",
models: ["am-parakeet-v2", "am-parakeet-v3"] satisfies SupportedSttModel[],
},
{
@@ -35,17 +35,17 @@ export const PROVIDERS = [
id: "deepgram",
displayName: "Deepgram",
icon: ,
- baseUrl: { value: "https://api.deepgram.com/v1", immutable: true },
+ baseUrl: "https://api.deepgram.com/v1",
models: ["nova-3", "nova-3-general", "nova-3-medical"],
},
{
disabled: false,
- id: "deepgram-custom",
- displayName: "Deepgram (Custom)",
+ id: "custom",
+ displayName: "Custom",
badge: null,
- icon: ,
- baseUrl: { value: "https://api.openai.com/v1", immutable: false },
- models: ["nova-3", "nova-3-general", "nova-3-medical"],
+ icon: ,
+ baseUrl: undefined,
+ models: [],
},
{
disabled: true,
@@ -53,7 +53,7 @@ export const PROVIDERS = [
displayName: "Groq",
badge: null,
icon: ,
- baseUrl: { value: "https://api.groq.com/v1", immutable: false },
+ baseUrl: "https://api.groq.com/v1",
models: ["whisper-large-v3-turbo", "whisper-large-v3"],
},
{
@@ -62,7 +62,7 @@ export const PROVIDERS = [
displayName: "Fireworks",
badge: null,
icon: ,
- baseUrl: { value: "https://api.firework.ai/v1", immutable: false },
+ baseUrl: "https://api.firework.ai/v1",
models: ["whisper-large-v3-turbo", "whisper-large-v3"],
},
] as const;
diff --git a/apps/desktop/src/hooks/useLLMConnection.ts b/apps/desktop/src/hooks/useLLMConnection.ts
index 4e689da06..3e39014d2 100644
--- a/apps/desktop/src/hooks/useLLMConnection.ts
+++ b/apps/desktop/src/hooks/useLLMConnection.ts
@@ -53,7 +53,7 @@ const useLLMConnection = (): {
const providerId = current_llm_provider as ProviderId;
const providerDefinition = PROVIDERS.find((provider) => provider.id === providerId);
- const baseUrl = providerConfig?.base_url?.trim() || providerDefinition?.baseUrl.value || "";
+ const baseUrl = providerConfig?.base_url?.trim() || providerDefinition?.baseUrl || "";
const apiKey = providerConfig?.api_key?.trim() || "";
if (!baseUrl) {
diff --git a/apps/desktop/src/store/tinybase/internal.ts b/apps/desktop/src/store/tinybase/internal.ts
index d9cee9304..f4bd39d55 100644
--- a/apps/desktop/src/store/tinybase/internal.ts
+++ b/apps/desktop/src/store/tinybase/internal.ts
@@ -26,8 +26,14 @@ export const generalSchema = z.object({
export const aiProviderSchema = z.object({
type: z.enum(["stt", "llm"]),
base_url: z.url().min(1),
- api_key: z.string().min(1),
-});
+ api_key: z.string(),
+}).refine(
+ (data) => !data.base_url.startsWith("https:") || data.api_key.length > 0,
+ {
+ message: "API key is required for HTTPS URLs",
+ path: ["api_key"],
+ },
+);
export type AIProvider = z.infer;
export type AIProviderStorage = ToStorageType;