added providers with easy provider configurations

This commit is contained in:
munimunigamer 2026-02-02 21:52:20 -06:00
parent efbb6830e1
commit d092bfecbb
18 changed files with 911 additions and 901 deletions

202
package-lock.json generated
View file

@ -10,8 +10,13 @@
"license": "AGPL-3.0",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.35",
"@ai-sdk/deepseek": "^2.0.17",
"@ai-sdk/google": "^3.0.20",
"@ai-sdk/groq": "^3.0.21",
"@ai-sdk/mistral": "^3.0.18",
"@ai-sdk/openai": "^3.0.25",
"@ai-sdk/openai-compatible": "^2.0.26",
"@ai-sdk/xai": "^3.0.46",
"@chutes-ai/ai-sdk-provider": "^0.1.2",
"@openrouter/ai-sdk-provider": "^2.1.1",
"@tauri-apps/api": "^2",
@ -33,7 +38,9 @@
"jszip": "^3.10.1",
"lucide-svelte": "^0.468.0",
"marked": "^17.0.1",
"tailwind-merge": "^3.4.0"
"ollama-ai-provider": "^1.2.0",
"tailwind-merge": "^3.4.0",
"zhipu-ai-provider": "^0.2.2"
},
"devDependencies": {
"@lucide/svelte": "^0.482.0",
@ -73,6 +80,22 @@
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/deepseek": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@ai-sdk/deepseek/-/deepseek-2.0.17.tgz",
"integrity": "sha512-rkZiasQ24UyOMiZd8Mb7R+OF3Yt90bRQyfyzIkrb0zKZj7kU2h2z2nu1CO6j0X8poE+SZhEEaHOBFhRcp6hKVg==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.7",
"@ai-sdk/provider-utils": "4.0.13"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/gateway": {
"version": "3.0.32",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.32.tgz",
@ -90,6 +113,54 @@
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/google": {
"version": "3.0.20",
"resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.20.tgz",
"integrity": "sha512-bVGsulEr6JiipAFlclo9bjL5WaUV0iCSiiekLt+PY6pwmtJeuU2GaD9DoE3OqR8LN2W779mU13IhVEzlTupf8g==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.7",
"@ai-sdk/provider-utils": "4.0.13"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/groq": {
"version": "3.0.21",
"resolved": "https://registry.npmjs.org/@ai-sdk/groq/-/groq-3.0.21.tgz",
"integrity": "sha512-sYTnGbvUNoDKTUa5BBKDvzSnjgahtEWriohdljtGBd4vgcimqY3XMXAqefOXEiYjON/GBFx6Q/YR3GVcX32Mcg==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.7",
"@ai-sdk/provider-utils": "4.0.13"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/mistral": {
"version": "3.0.18",
"resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-3.0.18.tgz",
"integrity": "sha512-k8nCBBVGOzBigNwBO5kREzsP/e+C3npcL7jt19ZdicIbZ6rvmnSIRI90iENyS9T10vM7sjrXoCpgZSYgJB2pJQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.7",
"@ai-sdk/provider-utils": "4.0.13"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/openai": {
"version": "3.0.25",
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.25.tgz",
@ -151,6 +222,23 @@
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/xai": {
"version": "3.0.46",
"resolved": "https://registry.npmjs.org/@ai-sdk/xai/-/xai-3.0.46.tgz",
"integrity": "sha512-26qM/jYcFhF5krTM7bQT1CiZcdz22EQmA+r5me1hKYFM/yM20sSUMHnAcUzvzuuG9oQVKF0tziU2IcC0HX5huQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/openai-compatible": "2.0.26",
"@ai-sdk/provider": "3.0.7",
"@ai-sdk/provider-utils": "4.0.13"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@chutes-ai/ai-sdk-provider": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@chutes-ai/ai-sdk-provider/-/ai-sdk-provider-0.1.2.tgz",
@ -2666,7 +2754,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@ -2688,12 +2775,69 @@
"dev": true,
"license": "MIT"
},
"node_modules/ollama-ai-provider": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ollama-ai-provider/-/ollama-ai-provider-1.2.0.tgz",
"integrity": "sha512-jTNFruwe3O/ruJeppI/quoOUxG7NA6blG3ZyQj3lei4+NnJo7bi3eIRWqlVpRlu/mbzbFXeJSBuYQWF6pzGKww==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "^1.0.0",
"@ai-sdk/provider-utils": "^2.0.0",
"partial-json": "0.1.7"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/ollama-ai-provider/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==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ollama-ai-provider/node_modules/@ai-sdk/provider-utils": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz",
"integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==",
"license": "Apache-2.0",
"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/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/partial-json": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz",
"integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -2864,6 +3008,12 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
@ -3280,6 +3430,48 @@
}
}
},
"node_modules/zhipu-ai-provider": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/zhipu-ai-provider/-/zhipu-ai-provider-0.2.2.tgz",
"integrity": "sha512-UjX1ho4DI9ICUv/mrpAnzmrRe5/LXrGkS5hF6h4WDY2aup5GketWWopFzWYCqsbArXAM5wbzzdH9QzZusgGiBg==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/zhipu-ai-provider/node_modules/@ai-sdk/provider": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.1.tgz",
"integrity": "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/zhipu-ai-provider/node_modules/@ai-sdk/provider-utils": {
"version": "3.0.20",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.20.tgz",
"integrity": "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.1",
"@standard-schema/spec": "^1.0.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
@ -3287,9 +3479,9 @@
"license": "MIT"
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {

View file

@ -14,8 +14,13 @@
"license": "AGPL-3.0",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.35",
"@ai-sdk/deepseek": "^2.0.17",
"@ai-sdk/google": "^3.0.20",
"@ai-sdk/groq": "^3.0.21",
"@ai-sdk/mistral": "^3.0.18",
"@ai-sdk/openai": "^3.0.25",
"@ai-sdk/openai-compatible": "^2.0.26",
"@ai-sdk/xai": "^3.0.46",
"@chutes-ai/ai-sdk-provider": "^0.1.2",
"@openrouter/ai-sdk-provider": "^2.1.1",
"@tauri-apps/api": "^2",
@ -37,7 +42,9 @@
"jszip": "^3.10.1",
"lucide-svelte": "^0.468.0",
"marked": "^17.0.1",
"tailwind-merge": "^3.4.0"
"ollama-ai-provider": "^1.2.0",
"tailwind-merge": "^3.4.0",
"zhipu-ai-provider": "^0.2.2"
},
"devDependencies": {
"@lucide/svelte": "^0.482.0",

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { ProviderType } from "$lib/types";
import { getProviderList } from "$lib/services/ai/sdk/providers/config";
import * as Select from "$lib/components/ui/select";
import { Label } from "$lib/components/ui/label";
@ -11,48 +12,7 @@
let { value, onchange, label = "Provider" }: Props = $props();
const providers: Array<{
value: ProviderType;
label: string;
description: string;
disabled?: boolean;
}> = [
{
value: "openrouter",
label: "OpenRouter",
description: "Access 100+ models from one API",
},
{
value: "openai",
label: "OpenAI (or compatible)",
description: "GPT, Azure, NIM, local LLMs, or any OpenAI-compatible API",
},
{
value: "anthropic",
label: "Anthropic",
description: "Claude models",
},
{
value: 'google',
label: 'Google AI',
description: 'Gemini models',
},
{
value: 'nanogpt',
label: 'NanoGPT',
description: 'Pay-as-you-go LLMs and image generation',
},
{
value: 'chutes',
label: 'Chutes',
description: 'Text and image generation',
},
{
value: 'pollinations',
label: 'Pollinations',
description: 'Free image generation (no API key needed)',
},
];
const providers = getProviderList();
function handleChange(newValue: string | undefined) {
if (newValue && newValue !== value) {
@ -60,7 +20,6 @@
}
}
// Find current provider for display
let currentProvider = $derived(providers.find((p) => p.value === value));
</script>
@ -78,16 +37,10 @@
</Select.Trigger>
<Select.Content>
{#each providers as provider}
<Select.Item
value={provider.value}
disabled={provider.disabled}
label={provider.label}
>
<Select.Item value={provider.value} label={provider.label}>
<div class="flex flex-col py-1">
<span class="font-medium">{provider.label}</span>
<span class="text-xs text-muted-foreground"
>{provider.description}</span
>
<span class="text-xs text-muted-foreground">{provider.description}</span>
</div>
</Select.Item>
{/each}

View file

@ -2,6 +2,7 @@
import { settings } from "$lib/stores/settings.svelte";
import type { APIProfile, ProviderType } from "$lib/types";
import { fetchModelsFromProvider } from "$lib/services/ai/sdk/providers";
import { PROVIDERS, hasDefaultEndpoint } from "$lib/services/ai/sdk/providers/config";
import ProviderTypeSelector from "$lib/components/settings/ProviderTypeSelector.svelte";
import {
Plus,
@ -16,7 +17,6 @@
Box,
AlertCircle,
Star,
Zap,
RotateCcw,
Search,
} from "lucide-svelte";
@ -63,38 +63,6 @@
let modelFilterInput = $state("");
let showBaseUrlCollapsible = $state(false);
// Provider defaults for base URLs
const providerDefaults: Record<ProviderType, string> = {
openrouter: "https://openrouter.ai/api/v1",
openai: "",
anthropic: "",
google: "",
nanogpt: "https://nano-gpt.com/api/v1",
chutes: "",
pollinations: "",
};
// Providers that have a built-in default API endpoint and can fetch models without a custom baseUrl
const providerHasDefaultEndpoint: Record<ProviderType, boolean> = {
openrouter: true,
openai: false,
anthropic: true,
google: true,
nanogpt: true,
chutes: true,
pollinations: true,
};
const providerDisplayNames: Record<ProviderType, string> = {
openrouter: "OpenRouter",
openai: "OpenAI Compatible",
anthropic: "Anthropic",
google: "Google AI",
nanogpt: "NanoGPT",
chutes: "Chutes",
pollinations: "Pollinations",
};
// Auto-save debounce state
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
@ -295,21 +263,6 @@
}
}
// Quick-fill presets for OpenAI-compatible endpoints
function quickFillOpenai() {
formName = "OpenAI";
formBaseUrl = "https://api.openai.com/v1";
}
function quickFillNvidianim() {
formName = "NVIDIA NIM";
formBaseUrl = "https://integrate.api.nvidia.com/v1";
}
function quickFillSelfHosted(port: string, name: string) {
formName = name;
formBaseUrl = `http://127.0.0.1:${port}/v1`;
}
function handleOpenChange(open: boolean, profile: APIProfile) {
if (open) {
@ -414,7 +367,7 @@
value={formProviderType}
onchange={(v) => {
formProviderType = v;
formName = providerDisplayNames[v];
formName = PROVIDERS[v].name;
formBaseUrl = "";
formFetchedModels = [];
formCustomModels = [];
@ -426,58 +379,7 @@
}}
/>
{#if formProviderType === "openai"}
<div class="space-y-2">
<div class="flex items-center gap-2 text-xs text-muted-foreground">
<Zap class="h-3 w-3" />
<span>Quick fill:</span>
</div>
<div class="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onclick={quickFillOpenai}
class="text-xs h-8"
>
OpenAI
</Button>
<Button
variant="outline"
size="sm"
onclick={quickFillNvidianim}
class="text-xs h-8"
>
NVIDIA NIM
</Button>
<Button
variant="outline"
size="sm"
onclick={() => quickFillSelfHosted("11434", "Ollama")}
class="text-xs h-8"
>
Ollama
</Button>
<Button
variant="outline"
size="sm"
onclick={() => quickFillSelfHosted("1234", "LM Studio")}
class="text-xs h-8"
>
LM Studio
</Button>
<Button
variant="outline"
size="sm"
onclick={() => quickFillSelfHosted("8080", "llama.cpp")}
class="text-xs h-8"
>
llama.cpp
</Button>
</div>
</div>
{/if}
{#if formProviderType === "openai"}
{#if formProviderType === "openai-compatible"}
<div class="space-y-2">
<Label for="new-url">
Base URL <span class="text-muted-foreground">(required)</span>
@ -501,7 +403,7 @@
{#if showBaseUrlCollapsible || formBaseUrl}
<Input
id="new-url"
placeholder={providerDefaults[formProviderType] || "https://api.example.com/v1"}
placeholder={PROVIDERS[formProviderType].baseUrl || "https://api.example.com/v1"}
bind:value={formBaseUrl}
class="font-mono text-xs"
/>
@ -545,7 +447,7 @@
variant="outline"
size="sm"
onclick={handleFetchModels}
disabled={isFetchingModels || (!formBaseUrl && !providerHasDefaultEndpoint[formProviderType])}
disabled={isFetchingModels || (!formBaseUrl && !hasDefaultEndpoint(formProviderType))}
>
{#if isFetchingModels}
<RefreshCw class="h-4 w-4 animate-spin" />
@ -624,7 +526,7 @@
>
<Button
onclick={handleSave}
disabled={!formName.trim() || (formProviderType === "openai" && !formBaseUrl.trim())}
disabled={!formName.trim() || (formProviderType === "openai-compatible" && !formBaseUrl.trim())}
class="flex-1"
>
<Check class="h-4 w-4" />
@ -750,58 +652,7 @@
}}
/>
{#if formProviderType === "openai"}
<div class="space-y-2">
<div class="flex items-center gap-2 text-xs text-muted-foreground">
<Zap class="h-3 w-3" />
<span>Quick fill:</span>
</div>
<div class="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onclick={quickFillOpenai}
class="text-xs h-8"
>
OpenAI
</Button>
<Button
variant="outline"
size="sm"
onclick={quickFillNvidianim}
class="text-xs h-8"
>
NVIDIA NIM
</Button>
<Button
variant="outline"
size="sm"
onclick={() => quickFillSelfHosted("11434", "Ollama")}
class="text-xs h-8"
>
Ollama
</Button>
<Button
variant="outline"
size="sm"
onclick={() => quickFillSelfHosted("1234", "LM Studio")}
class="text-xs h-8"
>
LM Studio
</Button>
<Button
variant="outline"
size="sm"
onclick={() => quickFillSelfHosted("8080", "llama.cpp")}
class="text-xs h-8"
>
llama.cpp
</Button>
</div>
</div>
{/if}
{#if formProviderType === "openai"}
{#if formProviderType === "openai-compatible"}
<div class="flex flex-col">
<Label class="mb-2">
Base URL <span class="text-muted-foreground text-xs">(required)</span>
@ -824,7 +675,7 @@
{#if showBaseUrlCollapsible || formBaseUrl}
<Input
bind:value={formBaseUrl}
placeholder={providerDefaults[formProviderType] || "https://api.example.com/v1"}
placeholder={PROVIDERS[formProviderType].baseUrl || "https://api.example.com/v1"}
class="font-mono text-xs"
/>
<p class="text-xs text-muted-foreground mt-1">
@ -863,7 +714,7 @@
variant="outline"
size="sm"
onclick={handleFetchModels}
disabled={isFetchingModels || (!formBaseUrl && !providerHasDefaultEndpoint[formProviderType])}
disabled={isFetchingModels || (!formBaseUrl && !hasDefaultEndpoint(formProviderType))}
>
{#if isFetchingModels}
<RefreshCw class="h-3 w-3 animate-spin" />
@ -1043,7 +894,7 @@
<div class="grid gap-1">
<Label class="text-muted-foreground text-xs">Base URL</Label>
<div class="font-mono text-sm bg-muted p-2 rounded truncate">
{profile.baseUrl || providerDefaults[profile.providerType] || "(default)"}
{profile.baseUrl || PROVIDERS[profile.providerType].baseUrl || "(default)"}
</div>
</div>

View file

@ -7,7 +7,7 @@
import { Slider } from "$lib/components/ui/slider";
import { RotateCcw } from "lucide-svelte";
import { listImageModels, clearModelsCache, type ImageModelInfo } from "$lib/services/ai/image/modelListing";
import { PROVIDER_CAPABILITIES } from "$lib/services/ai/sdk/providers/defaults";
import { PROVIDERS } from "$lib/services/ai/sdk/providers/config";
import ImageModelSelect from "$lib/components/settings/ImageModelSelect.svelte";
import type { APIProfile } from "$lib/types";
@ -26,7 +26,7 @@
// Get profiles that support image generation
function getImageCapableProfiles(): APIProfile[] {
return settings.apiSettings.profiles.filter(p =>
PROVIDER_CAPABILITIES[p.providerType]?.supportsImageGeneration
PROVIDERS[p.providerType]?.capabilities.imageGeneration
);
}

View file

@ -13,7 +13,7 @@
import type { Character, EmbeddedImage } from '$lib/types';
import { generateImage as sdkGenerateImage } from '$lib/services/ai/sdk/generate';
import { PROVIDER_CAPABILITIES } from '$lib/services/ai/sdk/providers/defaults';
import { PROVIDERS } from '$lib/services/ai/sdk/providers/config';
import { database } from '$lib/services/database';
import { promptService } from '$lib/services/prompts';
import { settings } from '$lib/stores/settings.svelte';
@ -50,7 +50,7 @@ export class InlineImageGenerationService {
if (!profile) return false;
// Check if provider supports image generation
const capabilities = PROVIDER_CAPABILITIES[profile.providerType];
const capabilities = PROVIDERS[profile.providerType].capabilities;
return capabilities?.supportsImageGeneration ?? false;
}

View file

@ -16,7 +16,7 @@
import { extractPicTags, type ParsedPicTag } from '$lib/utils/inlineImageParser';
import { generateImage as sdkGenerateImage } from '$lib/services/ai/sdk/generate';
import { PROVIDER_CAPABILITIES } from '$lib/services/ai/sdk/providers/defaults';
import { PROVIDERS } from '$lib/services/ai/sdk/providers/config';
import { database } from '$lib/services/database';
import { promptService } from '$lib/services/prompts';
import { settings } from '$lib/stores/settings.svelte';
@ -122,7 +122,7 @@ export class InlineImageTracker {
// Check if provider supports image generation
const profile = settings.getProfile(profileId);
if (!profile) return;
const capabilities = PROVIDER_CAPABILITIES[profile.providerType];
const capabilities = PROVIDERS[profile.providerType].capabilities;
if (!capabilities?.supportsImageGeneration) return;
// Build full prompt with style

View file

@ -7,7 +7,7 @@
import type { EmbeddedImage } from '$lib/types';
import { generateImage } from '$lib/services/ai/sdk/generate';
import { PROVIDER_CAPABILITIES } from '$lib/services/ai/sdk/providers/defaults';
import { PROVIDERS } from '$lib/services/ai/sdk/providers/config';
import { database } from '$lib/services/database';
import { settings } from '$lib/stores/settings.svelte';
import { emitImageReady, emitImageAnalysisFailed } from '$lib/services/events';
@ -29,7 +29,7 @@ export function isImageGenerationEnabled(): boolean {
const profile = settings.getProfile(profileId);
if (!profile) return false;
const capabilities = PROVIDER_CAPABILITIES[profile.providerType];
const capabilities = PROVIDERS[profile.providerType].capabilities;
return capabilities?.supportsImageGeneration ?? false;
}
@ -46,7 +46,7 @@ export function hasRequiredCredentials(): boolean {
if (!profile) return false;
// Check if provider supports image generation
const capabilities = PROVIDER_CAPABILITIES[profile.providerType];
const capabilities = PROVIDERS[profile.providerType].capabilities;
if (!capabilities?.supportsImageGeneration) return false;
// All profile-based providers have credentials if the profile exists

View file

@ -25,7 +25,7 @@ import { settings } from '$lib/stores/settings.svelte';
import type { ProviderType, GenerationPreset, ReasoningEffort, APIProfile } from '$lib/types';
import { createLogger } from '../core/config';
import { createProviderFromProfile } from './providers';
import { PROVIDER_CAPABILITIES } from './providers/defaults';
import { PROVIDERS } from './providers/config';
import { promptSchemaMiddleware, patchResponseMiddleware, loggingMiddleware } from './middleware';
const log = createLogger('Generate');
@ -54,12 +54,22 @@ interface GenerateObjectOptions<T extends z.ZodType> extends BaseGenerateOptions
const PROVIDER_OPTIONS_KEY: Record<ProviderType, string> = {
openrouter: 'openrouter',
openai: 'openai',
anthropic: 'anthropic',
google: 'google',
nanogpt: 'nanogpt',
chutes: 'chutes',
pollinations: 'pollinations',
ollama: 'ollama',
lmstudio: 'lmstudio',
llamacpp: 'llamacpp',
'nvidia-nim': 'openai',
'openai-compatible': 'openai',
openai: 'openai',
anthropic: 'anthropic',
google: 'google',
xai: 'xai',
groq: 'groq',
zhipu: 'zhipu',
deepseek: 'deepseek',
mistral: 'mistral',
};
const ANTHROPIC_REASONING_BUDGETS: Record<ReasoningEffort, number> = {
@ -152,7 +162,7 @@ function resolveConfig(presetId: string): ResolvedConfig {
const provider = createProviderFromProfile(profile);
const model = provider(preset.model) as LanguageModelV3;
const capabilities = PROVIDER_CAPABILITIES[profile.providerType];
const capabilities = PROVIDERS[profile.providerType].capabilities;
return {
preset,
@ -405,7 +415,7 @@ export async function generateImage(options: GenerateImageOptions): Promise<Gene
throw new Error(`Profile not found: ${profileId}`);
}
const capabilities = PROVIDER_CAPABILITIES[profile.providerType];
const capabilities = PROVIDERS[profile.providerType].capabilities;
if (!capabilities?.supportsImageGeneration) {
throw new Error(`Provider ${profile.providerType} does not support image generation`);
}

View file

@ -26,7 +26,7 @@ export {
} from './generate';
// Provider registry
export { createProviderFromProfile, PROVIDER_DEFAULTS } from './providers';
export { createProviderFromProfile, PROVIDERS } from './providers';
// Agent factory and stop conditions
export {
@ -50,7 +50,7 @@ export {
// Types
export type { ProviderType, APIProfile } from '$lib/types';
export type { ProviderDefaults, ServiceModelDefaults } from './providers';
export type { ProviderConfig, ServiceModelDefaults } from './providers';
export type { ResolvedAgentConfig, CreateAgentOptions, AgentResult } from './agents';
export type { LorebookToolContext, LorebookTools, FandomToolContext, FandomTools, RetrievalToolContext, RetrievalTools } from './tools';

View file

@ -0,0 +1,391 @@
/**
* Unified Provider Configuration
*
* Single source of truth for all provider metadata, defaults, and capabilities.
*/
import type { ProviderType, ReasoningEffort } from '$lib/types';
// ============================================================================
// Types
// ============================================================================
export interface ServiceModelDefaults {
model: string;
temperature: number;
maxTokens: number;
reasoningEffort: ReasoningEffort;
}
export interface ProviderCapabilities {
textGeneration: boolean;
imageGeneration: boolean;
structuredOutput: boolean;
}
export interface ImageDefaults {
defaultModel: string;
referenceModel: string;
supportedSizes: string[];
}
export interface ProviderConfig {
name: string;
description: string;
baseUrl: string; // Empty string = SDK default
requiresApiKey: boolean;
capabilities: ProviderCapabilities;
imageDefaults?: ImageDefaults;
fallbackModels: string[];
services: {
narrative: ServiceModelDefaults;
classification: ServiceModelDefaults;
memory: ServiceModelDefaults;
suggestions: ServiceModelDefaults;
agentic: ServiceModelDefaults;
wizard: ServiceModelDefaults;
translation: ServiceModelDefaults;
};
}
// ============================================================================
// Provider Configurations
// ============================================================================
export const PROVIDERS: Record<ProviderType, ProviderConfig> = {
openrouter: {
name: 'OpenRouter',
description: 'Access 100+ models from one API',
baseUrl: 'https://openrouter.ai/api/v1',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['anthropic/claude-sonnet-4', 'openai/gpt-4o', 'google/gemini-2.0-flash', 'deepseek/deepseek-chat', 'x-ai/grok-3'],
services: {
narrative: { model: 'anthropic/claude-sonnet-4', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'x-ai/grok-4.1-fast', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'high' },
memory: { model: 'x-ai/grok-4.1-fast', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'high' },
suggestions: { model: 'deepseek/deepseek-chat', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'anthropic/claude-sonnet-4', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'high' },
wizard: { model: 'deepseek/deepseek-chat', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'deepseek/deepseek-chat', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
nanogpt: {
name: 'NanoGPT',
description: 'Subscription-Based LLMs and image generation',
baseUrl: 'https://nano-gpt.com/api/v1',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: true, structuredOutput: false },
imageDefaults: { defaultModel: 'z-image-turbo', referenceModel: 'qwen-image', supportedSizes: ['512x512', '1024x1024', '2048x2048'] },
fallbackModels: ['deepseek-chat', 'claude-sonnet-4', 'gpt-4o', 'gemini-2.0-flash'],
services: {
narrative: { model: 'zai-or/glm-4.7', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'zai-org/glm-4.7', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'deepseek-chat', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'deepseek-chat', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'deepseek-chat', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'deepseek-chat', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'deepseek-chat', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
chutes: {
name: 'Chutes',
description: 'Text and image generation',
baseUrl: 'https://api.chutes.ai',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: true, structuredOutput: true },
imageDefaults: { defaultModel: 'z-image-turbo', referenceModel: 'qwen-image-edit-2511', supportedSizes: ['576x576', '1024x1024', '2048x2048'] },
fallbackModels: ['deepseek-ai/DeepSeek-V3-0324', 'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8'],
services: {
narrative: { model: 'deepseek-ai/DeepSeek-V3-0324', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'deepseek-ai/DeepSeek-V3-0324', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'deepseek-ai/DeepSeek-V3-0324', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'deepseek-ai/DeepSeek-V3-0324', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'deepseek-ai/DeepSeek-V3-0324', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'deepseek-ai/DeepSeek-V3-0324', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'deepseek-ai/DeepSeek-V3-0324', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
pollinations: {
name: 'Pollinations',
description: 'Free text and image generation (no API key needed)',
baseUrl: 'https://text.pollinations.ai/openai',
requiresApiKey: false,
capabilities: { textGeneration: true, imageGeneration: true, structuredOutput: false },
imageDefaults: { defaultModel: 'flux', referenceModel: 'kontext', supportedSizes: ['512x512', '1024x1024', '2048x2048'] },
fallbackModels: ['openai', 'mistral', 'llama'],
services: {
narrative: { model: 'openai', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'openai', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'openai', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'openai', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'openai', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'openai', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'openai', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
ollama: {
name: 'Ollama',
description: 'Run local LLMs (requires Ollama installed)',
baseUrl: 'http://localhost:11434',
requiresApiKey: false,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'qwen2.5', 'phi3', 'gemma2'],
services: {
narrative: { model: 'llama3.2', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'llama3.2', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'llama3.2', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'llama3.2', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'llama3.2', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'llama3.2', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'llama3.2', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
lmstudio: {
name: 'LM Studio',
description: 'Run local LLMs (requires LM Studio installed)',
baseUrl: 'http://localhost:1234/v1',
requiresApiKey: false,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: false },
fallbackModels: ['loaded-model'],
services: {
narrative: { model: 'loaded-model', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'loaded-model', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'loaded-model', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'loaded-model', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'loaded-model', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'loaded-model', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'loaded-model', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
llamacpp: {
name: 'llama.cpp',
description: 'Run local LLMs (requires llama.cpp server)',
baseUrl: 'http://localhost:8080/v1',
requiresApiKey: false,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: false },
fallbackModels: ['loaded-model'],
services: {
narrative: { model: 'loaded-model', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'loaded-model', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'loaded-model', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'loaded-model', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'loaded-model', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'loaded-model', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'loaded-model', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
'nvidia-nim': {
name: 'NVIDIA NIM',
description: 'NVIDIA hosted inference microservices',
baseUrl: 'https://integrate.api.nvidia.com/v1',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['meta/llama-3.1-70b-instruct', 'meta/llama-3.1-8b-instruct', 'nvidia/llama-3.1-nemotron-70b-instruct'],
services: {
narrative: { model: 'meta/llama-3.1-70b-instruct', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'meta/llama-3.1-8b-instruct', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'meta/llama-3.1-8b-instruct', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'meta/llama-3.1-8b-instruct', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'meta/llama-3.1-70b-instruct', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'meta/llama-3.1-8b-instruct', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'meta/llama-3.1-8b-instruct', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
'openai-compatible': {
name: 'OpenAI Compatible',
description: 'Any OpenAI-compatible API (requires custom URL)',
baseUrl: '', // Requires custom baseUrl
requiresApiKey: false,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: false },
fallbackModels: ['default'],
services: {
narrative: { model: 'default', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'default', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'default', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'default', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'default', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'default', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'default', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
openai: {
name: 'OpenAI',
description: 'GPT models from OpenAI',
baseUrl: '', // SDK default
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: true, structuredOutput: true },
imageDefaults: { defaultModel: 'dall-e-3', referenceModel: 'dall-e-2', supportedSizes: ['1024x1024', '1024x1792', '1792x1024'] },
fallbackModels: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo', 'o1', 'o1-mini'],
services: {
narrative: { model: 'gpt-4o', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'gpt-4o-mini', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'gpt-4o-mini', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'gpt-4o-mini', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'gpt-4o', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'gpt-4o-mini', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'gpt-4o-mini', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
anthropic: {
name: 'Anthropic',
description: 'Claude models',
baseUrl: '', // SDK default
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['claude-opus-4-5-20251101', 'claude-haiku-4-5-20251001', 'claude-sonnet-4-5-20250929', 'claude-opus-4-1-20250805', 'claude-sonnet-4-20250514', 'claude-opus-4-20250514'],
services: {
narrative: { model: 'claude-sonnet-4-20250514', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'claude-haiku-4-20250514', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'claude-haiku-4-20250514', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'claude-haiku-4-20250514', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'claude-sonnet-4-20250514', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'claude-haiku-4-20250514', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'claude-haiku-4-20250514', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
google: {
name: 'Google AI',
description: 'Gemini models',
baseUrl: '', // SDK default
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: true, structuredOutput: true },
imageDefaults: { defaultModel: 'imagen-3.0-generate-002', referenceModel: 'imagen-3.0-generate-002', supportedSizes: ['512x512', '1024x1024'] },
fallbackModels: ['gemini-3-pro-preview', 'gemini-3-flash-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],
services: {
narrative: { model: 'gemini-2.0-flash', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'gemini-2.0-flash', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'gemini-2.0-flash', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'gemini-2.0-flash', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'gemini-2.0-flash', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'gemini-2.0-flash', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'gemini-2.0-flash', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
xai: {
name: 'xAI (Grok)',
description: 'Grok models from xAI',
baseUrl: 'https://api.x.ai/v1',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['grok-3', 'grok-3-fast', 'grok-2', 'grok-2-vision'],
services: {
narrative: { model: 'grok-3', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'grok-3-fast', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'grok-3-fast', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'grok-3-fast', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'grok-3', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'grok-3-fast', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'grok-3-fast', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
groq: {
name: 'Groq',
description: 'Ultra-fast inference for open models',
baseUrl: 'https://api.groq.com/openai/v1',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768', 'gemma2-9b-it'],
services: {
narrative: { model: 'llama-3.3-70b-versatile', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'llama-3.1-8b-instant', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'llama-3.1-8b-instant', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'llama-3.1-8b-instant', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'llama-3.3-70b-versatile', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'llama-3.1-8b-instant', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'llama-3.1-8b-instant', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
zhipu: {
name: 'Zhipu AI',
description: 'GLM models (Chinese AI provider)',
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: true, structuredOutput: true },
imageDefaults: { defaultModel: 'cogview-3-plus', referenceModel: 'cogview-3', supportedSizes: ['512x512', '1024x1024'] },
fallbackModels: ['glm-4-plus', 'glm-4-flash', 'glm-4-air', 'glm-4v', 'glm-4v-plus', 'cogview-3-plus'],
services: {
narrative: { model: 'glm-4-plus', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'glm-4-flash', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'glm-4-flash', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'glm-4-flash', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'glm-4-plus', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'glm-4-flash', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'glm-4-flash', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
deepseek: {
name: 'DeepSeek',
description: 'Cost-effective reasoning models',
baseUrl: 'https://api.deepseek.com/v1',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['deepseek-chat', 'deepseek-reasoner'],
services: {
narrative: { model: 'deepseek-chat', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'deepseek-chat', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'deepseek-chat', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'deepseek-chat', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'deepseek-chat', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'deepseek-chat', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'deepseek-chat', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
mistral: {
name: 'Mistral',
description: 'European AI provider with strong coding models',
baseUrl: 'https://api.mistral.ai/v1',
requiresApiKey: true,
capabilities: { textGeneration: true, imageGeneration: false, structuredOutput: true },
fallbackModels: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest', 'pixtral-large-latest', 'ministral-8b-latest', 'ministral-3b-latest'],
services: {
narrative: { model: 'mistral-large-latest', temperature: 0.8, maxTokens: 8192, reasoningEffort: 'off' },
classification: { model: 'mistral-small-latest', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
memory: { model: 'mistral-small-latest', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
suggestions: { model: 'mistral-small-latest', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
agentic: { model: 'mistral-large-latest', temperature: 0.3, maxTokens: 8192, reasoningEffort: 'off' },
wizard: { model: 'mistral-small-latest', temperature: 0.7, maxTokens: 8192, reasoningEffort: 'off' },
translation: { model: 'mistral-small-latest', temperature: 0.3, maxTokens: 4096, reasoningEffort: 'off' },
},
},
};
// ============================================================================
// Helper Functions
// ============================================================================
/** Get the base URL for a provider, or undefined if SDK default should be used */
export function getBaseUrl(providerType: ProviderType): string | undefined {
const url = PROVIDERS[providerType].baseUrl;
return url || undefined;
}
/** Check if a provider has a default endpoint (doesn't require custom URL) */
export function hasDefaultEndpoint(providerType: ProviderType): boolean {
return providerType !== 'openai-compatible';
}
/** Get all providers as a list for UI dropdowns */
export function getProviderList(): Array<{ value: ProviderType; label: string; description: string }> {
return (Object.keys(PROVIDERS) as ProviderType[]).map((key) => ({
value: key,
label: PROVIDERS[key].name,
description: PROVIDERS[key].description,
}));
}

View file

@ -1,426 +0,0 @@
/**
* Provider Defaults Configuration
*
* Defines default models and settings for each provider type.
*/
import type { ProviderType, ReasoningEffort } from '$lib/types';
// ============================================================================
// API URLs
// ============================================================================
export const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1';
export const NANOGPT_API_URL = 'https://nano-gpt.com/api/v1';
// ============================================================================
// Provider Capabilities
// ============================================================================
export interface ProviderCapabilities {
supportsTextGeneration: boolean;
supportsImageGeneration: boolean;
supportsStructuredOutput: boolean;
}
export const PROVIDER_CAPABILITIES: Record<ProviderType, ProviderCapabilities> = {
openrouter: { supportsTextGeneration: true, supportsImageGeneration: false, supportsStructuredOutput: true },
openai: { supportsTextGeneration: true, supportsImageGeneration: true, supportsStructuredOutput: true },
anthropic: { supportsTextGeneration: true, supportsImageGeneration: false, supportsStructuredOutput: true },
google: { supportsTextGeneration: true, supportsImageGeneration: true, supportsStructuredOutput: true },
nanogpt: { supportsTextGeneration: true, supportsImageGeneration: true, supportsStructuredOutput: false },
chutes: { supportsTextGeneration: true, supportsImageGeneration: true, supportsStructuredOutput: true },
pollinations: { supportsTextGeneration: true, supportsImageGeneration: true, supportsStructuredOutput: false },
};
// ============================================================================
// Image Model Defaults
// ============================================================================
export interface ImageModelDefaults {
defaultModel: string;
referenceModel: string;
supportedSizes: string[];
}
export const IMAGE_MODEL_DEFAULTS: Partial<Record<ProviderType, ImageModelDefaults>> = {
openai: {
defaultModel: 'dall-e-3',
referenceModel: 'dall-e-2', // DALL-E 2 supports image editing
supportedSizes: ['1024x1024', '1024x1792', '1792x1024'],
},
google: {
defaultModel: 'imagen-3.0-generate-002',
referenceModel: 'imagen-3.0-generate-002',
supportedSizes: ['512x512', '1024x1024'],
},
nanogpt: {
defaultModel: 'z-image-turbo',
referenceModel: 'qwen-image',
supportedSizes: ['512x512', '1024x1024', '2048x2048'],
},
chutes: {
defaultModel: 'z-image-turbo',
referenceModel: 'qwen-image-edit-2511',
supportedSizes: ['576x576', '1024x1024', '2048x2048'],
},
pollinations: {
defaultModel: 'flux',
referenceModel: 'kontext',
supportedSizes: ['512x512', '1024x1024', '2048x2048'],
},
};
// ============================================================================
// Service Defaults
// ============================================================================
export interface ServiceModelDefaults {
model: string;
temperature: number;
maxTokens: number;
reasoningEffort: ReasoningEffort;
}
export interface ProviderDefaults {
name: string;
baseUrl: string;
narrative: ServiceModelDefaults;
classification: ServiceModelDefaults;
memory: ServiceModelDefaults;
suggestions: ServiceModelDefaults;
agentic: ServiceModelDefaults;
wizard: ServiceModelDefaults;
translation: ServiceModelDefaults;
}
export const PROVIDER_DEFAULTS: Record<ProviderType, ProviderDefaults> = {
openrouter: {
name: 'OpenRouter',
baseUrl: 'https://openrouter.ai/api/v1',
narrative: {
model: 'anthropic/claude-sonnet-4',
temperature: 0.8,
maxTokens: 8192,
reasoningEffort: 'off',
},
classification: {
model: 'x-ai/grok-4.1-fast',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'high',
},
memory: {
model: 'x-ai/grok-4.1-fast',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'high',
},
suggestions: {
model: 'deepseek/deepseek-chat',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
agentic: {
model: 'anthropic/claude-sonnet-4',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'high',
},
wizard: {
model: 'deepseek/deepseek-chat',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
translation: {
model: 'deepseek/deepseek-chat',
temperature: 0.3,
maxTokens: 4096,
reasoningEffort: 'off',
},
},
openai: {
name: 'OpenAI',
baseUrl: '', // SDK default
narrative: {
model: 'gpt-4o',
temperature: 0.8,
maxTokens: 8192,
reasoningEffort: 'off',
},
classification: {
model: 'gpt-4o-mini',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
memory: {
model: 'gpt-4o-mini',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
suggestions: {
model: 'gpt-4o-mini',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
agentic: {
model: 'gpt-4o',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
wizard: {
model: 'gpt-4o-mini',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
translation: {
model: 'gpt-4o-mini',
temperature: 0.3,
maxTokens: 4096,
reasoningEffort: 'off',
},
},
anthropic: {
name: 'Anthropic',
baseUrl: '', // SDK default
narrative: {
model: 'claude-sonnet-4-20250514',
temperature: 0.8,
maxTokens: 8192,
reasoningEffort: 'off',
},
classification: {
model: 'claude-haiku-4-20250514',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
memory: {
model: 'claude-haiku-4-20250514',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
suggestions: {
model: 'claude-haiku-4-20250514',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
agentic: {
model: 'claude-sonnet-4-20250514',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
wizard: {
model: 'claude-haiku-4-20250514',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
translation: {
model: 'claude-haiku-4-20250514',
temperature: 0.3,
maxTokens: 4096,
reasoningEffort: 'off',
},
},
google: {
name: 'Google AI',
baseUrl: '', // SDK default
narrative: {
model: 'gemini-2.0-flash',
temperature: 0.8,
maxTokens: 8192,
reasoningEffort: 'off',
},
classification: {
model: 'gemini-2.0-flash',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
memory: {
model: 'gemini-2.0-flash',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
suggestions: {
model: 'gemini-2.0-flash',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
agentic: {
model: 'gemini-2.0-flash',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
wizard: {
model: 'gemini-2.0-flash',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
translation: {
model: 'gemini-2.0-flash',
temperature: 0.3,
maxTokens: 4096,
reasoningEffort: 'off',
},
},
nanogpt: {
name: 'NanoGPT',
baseUrl: 'https://nano-gpt.com/api/v1',
narrative: {
model: 'deepseek-chat',
temperature: 0.8,
maxTokens: 8192,
reasoningEffort: 'off',
},
classification: {
model: 'deepseek-chat',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
memory: {
model: 'deepseek-chat',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
suggestions: {
model: 'deepseek-chat',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
agentic: {
model: 'deepseek-chat',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
wizard: {
model: 'deepseek-chat',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
translation: {
model: 'deepseek-chat',
temperature: 0.3,
maxTokens: 4096,
reasoningEffort: 'off',
},
},
chutes: {
name: 'Chutes',
baseUrl: '', // SDK default
narrative: {
model: 'deepseek-ai/DeepSeek-V3-0324',
temperature: 0.8,
maxTokens: 8192,
reasoningEffort: 'off',
},
classification: {
model: 'deepseek-ai/DeepSeek-V3-0324',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
memory: {
model: 'deepseek-ai/DeepSeek-V3-0324',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
suggestions: {
model: 'deepseek-ai/DeepSeek-V3-0324',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
agentic: {
model: 'deepseek-ai/DeepSeek-V3-0324',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
wizard: {
model: 'deepseek-ai/DeepSeek-V3-0324',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
translation: {
model: 'deepseek-ai/DeepSeek-V3-0324',
temperature: 0.3,
maxTokens: 4096,
reasoningEffort: 'off',
},
},
pollinations: {
name: 'Pollinations',
baseUrl: '', // SDK default
narrative: {
model: 'openai',
temperature: 0.8,
maxTokens: 8192,
reasoningEffort: 'off',
},
classification: {
model: 'openai',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
memory: {
model: 'openai',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
suggestions: {
model: 'openai',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
agentic: {
model: 'openai',
temperature: 0.3,
maxTokens: 8192,
reasoningEffort: 'off',
},
wizard: {
model: 'openai',
temperature: 0.7,
maxTokens: 8192,
reasoningEffort: 'off',
},
translation: {
model: 'openai',
temperature: 0.3,
maxTokens: 4096,
reasoningEffort: 'off',
},
},
};

View file

@ -5,7 +5,15 @@
*/
export { createProviderFromProfile } from './registry';
export { PROVIDER_DEFAULTS } from './defaults';
export { fetchModelsFromProvider } from './modelFetcher';
export type { ProviderDefaults, ServiceModelDefaults } from './defaults';
export {
PROVIDERS,
getBaseUrl,
hasDefaultEndpoint,
getProviderList,
type ProviderConfig,
type ServiceModelDefaults,
type ProviderCapabilities,
type ImageDefaults,
} from './config';
export type { ProviderType, APIProfile } from '$lib/types';

View file

@ -1,218 +1,103 @@
/**
* Model Fetcher
*
* Fetches available models from AI providers using SDK-compatible fetch infrastructure.
* Uses the same Tauri-compatible fetch that Vercel AI SDK providers use internally.
* Fetches available models from AI providers.
*/
import { createTimeoutFetch } from './fetch';
import { PROVIDERS, getBaseUrl } from './config';
import type { ProviderType } from '$lib/types';
/**
* Default base URLs for model fetching endpoints.
* Used when baseUrl is not explicitly provided.
*/
const DEFAULT_BASE_URLS: Record<ProviderType, string | undefined> = {
openrouter: 'https://openrouter.ai/api/v1',
openai: 'https://api.openai.com/v1',
anthropic: 'https://api.anthropic.com/v1',
google: 'https://generativelanguage.googleapis.com/v1beta',
nanogpt: 'https://nano-gpt.com/api/v1',
chutes: 'https://api.chutes.ai',
pollinations: 'https://gen.pollinations.ai/v1',
};
/** URLs that don't require authentication for model fetching */
const NO_AUTH_PATTERNS = ['nano-gpt.com', 'gen.pollinations.ai', '127.0.0.1', 'localhost'];
/**
* Fallback model list for Anthropic, used when the /v1/models endpoint fails.
*/
const ANTHROPIC_FALLBACK_MODELS = [
'claude-opus-4-5-20251101',
'claude-haiku-4-5-20251001',
'claude-sonnet-4-5-20250929',
'claude-opus-4-1-20250805',
'claude-sonnet-4-20250514',
'claude-opus-4-20250514',
];
/**
* URLs that don't require authentication for model fetching.
* Used for OpenAI-compatible providers that have public /models endpoints.
*/
const NO_AUTH_PATTERNS = [
'integrate.api.nvidia.com', // NVIDIA NIM
'nano-gpt.com', // NanoGPT
'gen.pollinations.ai', // Pollinations
'127.0.0.1', // Self-hosted (Ollama, LM Studio, llama.cpp)
'localhost', // Self-hosted alternative
];
/**
* Fetches available models from a provider using the SDK's fetch infrastructure.
*
* This function uses createTimeoutFetch() which wraps Tauri's HTTP plugin,
* making it compatible with Tauri's sandboxed environment and consistent
* with how the Vercel AI SDK providers make their API calls.
*
* @param providerType - The provider to fetch models from
* @param baseUrl - Optional custom base URL (for local LLMs, custom deployments, etc.)
* @param apiKey - Optional API key for authentication (required for some providers)
* @returns Array of model IDs available from the provider
* @throws Error if the provider doesn't support model fetching or if the request fails
*
* @example
* ```typescript
* // Fetch OpenRouter models
* const models = await fetchModelsFromProvider('openrouter');
*
* // Fetch OpenAI models with API key
* const models = await fetchModelsFromProvider('openai', undefined, apiKey);
*
* // Fetch from local Ollama instance
* const models = await fetchModelsFromProvider('openai', 'http://localhost:11434/v1');
* ```
* Fetches available models from a provider.
*/
export async function fetchModelsFromProvider(
providerType: ProviderType,
baseUrl?: string,
apiKey?: string
): Promise<string[]> {
// Google uses a different endpoint format with API key as query param
if (providerType === 'google') {
return fetchGoogleModels(baseUrl, apiKey);
}
// Provider-specific fetch logic
if (providerType === 'google') return fetchGoogleModels(baseUrl, apiKey);
if (providerType === 'anthropic') return fetchAnthropicModels(baseUrl, apiKey);
if (providerType === 'chutes') return fetchChutesModels(baseUrl, apiKey);
if (providerType === 'ollama') return fetchOllamaModels(baseUrl);
if (providerType === 'zhipu') return fetchZhipuModels(baseUrl, apiKey);
if (providerType === 'mistral') return fetchMistralModels(baseUrl, apiKey);
// Anthropic uses a different auth header and response format
if (providerType === 'anthropic') {
return fetchAnthropicModels(baseUrl, apiKey);
}
// Chutes uses a different endpoint format
if (providerType === 'chutes') {
return fetchChutesModels(baseUrl, apiKey);
}
// Determine effective base URL
const effectiveBaseUrl = baseUrl || DEFAULT_BASE_URLS[providerType];
// Standard OpenAI-compatible endpoint
const effectiveBaseUrl = baseUrl || getBaseUrl(providerType);
if (!effectiveBaseUrl) {
throw new Error(`No base URL available for provider: ${providerType}`);
}
// Check if this URL requires authentication for model fetching
const requiresAuth = !NO_AUTH_PATTERNS.some(pattern =>
effectiveBaseUrl.toLowerCase().includes(pattern)
);
// Create SDK-compatible fetch with 30s timeout (shorter than default for model fetching)
const requiresAuth = !NO_AUTH_PATTERNS.some((p) => effectiveBaseUrl.toLowerCase().includes(p));
const fetch = createTimeoutFetch(30000);
const modelsUrl = effectiveBaseUrl.replace(/\/$/, '') + '/models';
// Make the request using SDK's Tauri-compatible fetch
// Only include auth header if required AND apiKey is provided
const response = await fetch(modelsUrl, {
method: 'GET',
headers: (requiresAuth && apiKey) ? { Authorization: `Bearer ${apiKey}` } : {},
headers: requiresAuth && apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
});
if (!response.ok) {
throw new Error(
`Failed to fetch models: ${response.status} ${response.statusText}`
);
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
}
const data = await response.json();
// Handle OpenAI/OpenRouter format: { data: [{ id: "..." }] }
if (data.data && Array.isArray(data.data)) {
return data.data.map((m: { id: string }) => m.id);
}
// Handle alternative format: [{ id: "...", name: "..." }]
if (Array.isArray(data)) {
return data
.map((m: { id?: string; name?: string }) => m.id || m.name || '')
.filter(Boolean);
return data.map((m: { id?: string; name?: string }) => m.id || m.name || '').filter(Boolean);
}
throw new Error('Unexpected API response format');
}
/**
* Fetches models from the Anthropic API.
* Anthropic uses `x-api-key` header and a different response format: { data: [{ id, type }] }
* Falls back to a curated static list if the API call fails.
*/
async function fetchAnthropicModels(baseUrl?: string, apiKey?: string): Promise<string[]> {
const effectiveBaseUrl = baseUrl || DEFAULT_BASE_URLS.anthropic!;
const effectiveBaseUrl = baseUrl || 'https://api.anthropic.com/v1';
const modelsUrl = effectiveBaseUrl.replace(/\/$/, '') + '/models';
try {
const fetchFn = createTimeoutFetch(30000);
const headers: Record<string, string> = {
'anthropic-version': '2023-06-01',
};
if (apiKey) {
headers['x-api-key'] = apiKey;
}
const headers: Record<string, string> = { 'anthropic-version': '2023-06-01' };
if (apiKey) headers['x-api-key'] = apiKey;
const response = await fetchFn(modelsUrl, {
method: 'GET',
headers,
});
const response = await fetchFn(modelsUrl, { method: 'GET', headers });
if (!response.ok) {
console.warn(`[ModelFetcher] Anthropic API returned ${response.status}, using fallback models`);
return ANTHROPIC_FALLBACK_MODELS;
return PROVIDERS.anthropic.fallbackModels;
}
const data = await response.json();
if (data.data && Array.isArray(data.data)) {
const models = data.data
.map((m: { id: string }) => m.id)
.filter(Boolean);
const models = data.data.map((m: { id: string }) => m.id).filter(Boolean);
if (models.length > 0) return models;
}
console.warn('[ModelFetcher] Unexpected Anthropic response format, using fallback models');
return ANTHROPIC_FALLBACK_MODELS;
return PROVIDERS.anthropic.fallbackModels;
} catch (error) {
console.warn('[ModelFetcher] Failed to fetch Anthropic models, using fallback list:', error);
return ANTHROPIC_FALLBACK_MODELS;
console.warn('[ModelFetcher] Failed to fetch Anthropic models:', error);
return PROVIDERS.anthropic.fallbackModels;
}
}
/**
* Fallback model list for Google, used when the API call fails.
*/
const GOOGLE_FALLBACK_MODELS = [
'gemini-3-pro-preview',
'gemini-3-flash-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
];
/**
* Google AI model entry from the /v1beta/models endpoint.
*/
interface GoogleModelEntry {
name: string;
displayName?: string;
supportedGenerationMethods?: string[];
}
/**
* Fetches models from Google AI API.
* Google uses API key as a query param and a `{ models: [...] }` response format.
* Only returns models that support `generateContent`.
* Falls back to a curated static list if the API call fails.
*/
async function fetchGoogleModels(baseUrl?: string, apiKey?: string): Promise<string[]> {
const effectiveBaseUrl = baseUrl || DEFAULT_BASE_URLS.google!;
const effectiveBaseUrl = baseUrl || 'https://generativelanguage.googleapis.com/v1beta';
if (!apiKey) {
console.warn('[ModelFetcher] Google API key required for model fetching, using fallback models');
return GOOGLE_FALLBACK_MODELS;
console.warn('[ModelFetcher] Google API key required, using fallback models');
return PROVIDERS.google.fallbackModels;
}
const modelsUrl = effectiveBaseUrl.replace(/\/$/, '') + '/models?key=' + apiKey;
@ -223,11 +108,10 @@ async function fetchGoogleModels(baseUrl?: string, apiKey?: string): Promise<str
if (!response.ok) {
console.warn(`[ModelFetcher] Google API returned ${response.status}, using fallback models`);
return GOOGLE_FALLBACK_MODELS;
return PROVIDERS.google.fallbackModels;
}
const data = await response.json();
if (data.models && Array.isArray(data.models)) {
const models = (data.models as GoogleModelEntry[])
.filter((m) => m.supportedGenerationMethods?.includes('generateContent'))
@ -236,24 +120,19 @@ async function fetchGoogleModels(baseUrl?: string, apiKey?: string): Promise<str
if (models.length > 0) return models;
}
console.warn('[ModelFetcher] Unexpected Google response format, using fallback models');
return GOOGLE_FALLBACK_MODELS;
return PROVIDERS.google.fallbackModels;
} catch (error) {
console.warn('[ModelFetcher] Failed to fetch Google models, using fallback list:', error);
return GOOGLE_FALLBACK_MODELS;
console.warn('[ModelFetcher] Failed to fetch Google models:', error);
return PROVIDERS.google.fallbackModels;
}
}
/**
* Fetches models from the Chutes API.
* Chutes uses a `/chutes` endpoint with Bearer auth and returns `{ data: [{ name }] }`.
*/
async function fetchChutesModels(baseUrl?: string, apiKey?: string): Promise<string[]> {
if (!apiKey) {
throw new Error('Chutes requires an API key to fetch models');
}
const effectiveBaseUrl = baseUrl || 'https://api.chutes.ai';
const effectiveBaseUrl = baseUrl || PROVIDERS.chutes.baseUrl;
const chutesUrl = effectiveBaseUrl.replace(/\/$/, '') + '/chutes';
const fetchFn = createTimeoutFetch(30000);
@ -267,18 +146,106 @@ async function fetchChutesModels(baseUrl?: string, apiKey?: string): Promise<str
}
const data = await response.json();
if (data.data && Array.isArray(data.data)) {
return data.data
.map((m: { id?: string; name?: string }) => m.id || m.name || '')
.filter(Boolean);
return data.data.map((m: { id?: string; name?: string }) => m.id || m.name || '').filter(Boolean);
}
if (Array.isArray(data)) {
return data
.map((m: { id?: string; name?: string }) => m.id || m.name || '')
.filter(Boolean);
return data.map((m: { id?: string; name?: string }) => m.id || m.name || '').filter(Boolean);
}
throw new Error('Unexpected Chutes API response format');
}
async function fetchOllamaModels(baseUrl?: string): Promise<string[]> {
const effectiveBaseUrl = baseUrl || PROVIDERS.ollama.baseUrl;
const tagsUrl = effectiveBaseUrl.replace(/\/$/, '').replace(/\/api$/, '') + '/api/tags';
try {
const fetchFn = createTimeoutFetch(10000);
const response = await fetchFn(tagsUrl, { method: 'GET' });
if (!response.ok) {
console.warn(`[ModelFetcher] Ollama returned ${response.status}, using fallback models`);
return PROVIDERS.ollama.fallbackModels;
}
const data = await response.json();
if (data.models && Array.isArray(data.models)) {
const models = data.models.map((m: { name?: string; model?: string }) => m.name || m.model || '').filter(Boolean);
if (models.length > 0) return models;
}
return PROVIDERS.ollama.fallbackModels;
} catch (error) {
console.warn('[ModelFetcher] Failed to fetch Ollama models (is Ollama running?):', error);
return PROVIDERS.ollama.fallbackModels;
}
}
async function fetchZhipuModels(baseUrl?: string, apiKey?: string): Promise<string[]> {
if (!apiKey) {
console.warn('[ModelFetcher] Zhipu API key required, using fallback models');
return PROVIDERS.zhipu.fallbackModels;
}
const effectiveBaseUrl = baseUrl || PROVIDERS.zhipu.baseUrl;
const modelsUrl = effectiveBaseUrl.replace(/\/$/, '') + '/models';
try {
const fetchFn = createTimeoutFetch(30000);
const response = await fetchFn(modelsUrl, {
method: 'GET',
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
console.warn(`[ModelFetcher] Zhipu API returned ${response.status}, using fallback models`);
return PROVIDERS.zhipu.fallbackModels;
}
const data = await response.json();
if (data.data && Array.isArray(data.data)) {
const models = data.data.map((m: { id?: string }) => m.id || '').filter(Boolean);
if (models.length > 0) return models;
}
return PROVIDERS.zhipu.fallbackModels;
} catch (error) {
console.warn('[ModelFetcher] Failed to fetch Zhipu models:', error);
return PROVIDERS.zhipu.fallbackModels;
}
}
async function fetchMistralModels(baseUrl?: string, apiKey?: string): Promise<string[]> {
if (!apiKey) {
console.warn('[ModelFetcher] Mistral API key required, using fallback models');
return PROVIDERS.mistral.fallbackModels;
}
const effectiveBaseUrl = baseUrl || PROVIDERS.mistral.baseUrl;
const modelsUrl = effectiveBaseUrl.replace(/\/$/, '') + '/models';
try {
const fetchFn = createTimeoutFetch(30000);
const response = await fetchFn(modelsUrl, {
method: 'GET',
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
console.warn(`[ModelFetcher] Mistral API returned ${response.status}, using fallback models`);
return PROVIDERS.mistral.fallbackModels;
}
const data = await response.json();
if (data.data && Array.isArray(data.data)) {
const models = data.data.map((m: { id?: string }) => m.id || '').filter(Boolean);
if (models.length > 0) return models;
}
return PROVIDERS.mistral.fallbackModels;
} catch (error) {
console.warn('[ModelFetcher] Failed to fetch Mistral models:', error);
return PROVIDERS.mistral.fallbackModels;
}
}

View file

@ -1,40 +1,37 @@
/**
* Provider Registry
*
* Single entry point for creating Vercel AI SDK providers from APIProfile.
* Creates Vercel AI SDK providers from APIProfile.
*/
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { createChutes } from '@chutes-ai/ai-sdk-provider';
import { createPollinations } from 'ai-sdk-pollinations';
import { createOllama } from 'ollama-ai-provider';
import { createXai } from '@ai-sdk/xai';
import { createGroq } from '@ai-sdk/groq';
import { createZhipu } from 'zhipu-ai-provider';
import { createDeepSeek } from '@ai-sdk/deepseek';
import { createMistral } from '@ai-sdk/mistral';
import type { APIProfile, ProviderType } from '$lib/types';
import type { APIProfile } from '$lib/types';
import { createTimeoutFetch } from './fetch';
import { NANOGPT_API_URL } from './defaults';
import { PROVIDERS, getBaseUrl } from './config';
const DEFAULT_TIMEOUT_MS = 180000;
const DEFAULT_BASE_URLS: Record<ProviderType, string | undefined> = {
openrouter: 'https://openrouter.ai/api/v1',
openai: undefined,
anthropic: undefined,
google: undefined,
nanogpt: NANOGPT_API_URL,
chutes: undefined,
pollinations: undefined,
};
export function createProviderFromProfile(profile: APIProfile) {
const fetch = createTimeoutFetch(DEFAULT_TIMEOUT_MS);
const baseURL = profile.baseUrl || DEFAULT_BASE_URLS[profile.providerType];
const baseURL = profile.baseUrl || getBaseUrl(profile.providerType);
switch (profile.providerType) {
case 'openrouter':
return createOpenRouter({
apiKey: profile.apiKey,
baseURL: baseURL ?? 'https://openrouter.ai/api/v1',
baseURL: baseURL ?? PROVIDERS.openrouter.baseUrl,
headers: { 'HTTP-Referer': 'https://aventura.camp', 'X-Title': 'Aventura' },
fetch,
});
@ -46,13 +43,13 @@ export function createProviderFromProfile(profile: APIProfile) {
return createAnthropic({ apiKey: profile.apiKey, baseURL, fetch });
case 'google':
throw new Error('Google provider not yet implemented');
return createGoogleGenerativeAI({ apiKey: profile.apiKey, baseURL, fetch });
case 'nanogpt':
return createOpenAI({
name: 'nanogpt',
apiKey: profile.apiKey,
baseURL: baseURL ?? NANOGPT_API_URL,
baseURL: baseURL ?? PROVIDERS.nanogpt.baseUrl,
fetch,
});
@ -62,6 +59,59 @@ export function createProviderFromProfile(profile: APIProfile) {
case 'pollinations':
return createPollinations({ apiKey: profile.apiKey || undefined });
case 'ollama':
return createOllama({ baseURL: baseURL ?? PROVIDERS.ollama.baseUrl });
case 'lmstudio':
return createOpenAI({
name: 'lmstudio',
apiKey: profile.apiKey || 'lm-studio',
baseURL: baseURL ?? PROVIDERS.lmstudio.baseUrl,
fetch,
});
case 'llamacpp':
return createOpenAI({
name: 'llamacpp',
apiKey: profile.apiKey || 'llamacpp',
baseURL: baseURL ?? PROVIDERS.llamacpp.baseUrl,
fetch,
});
case 'nvidia-nim':
return createOpenAI({
name: 'nvidia-nim',
apiKey: profile.apiKey,
baseURL: baseURL ?? PROVIDERS['nvidia-nim'].baseUrl,
fetch,
});
case 'openai-compatible':
if (!baseURL) {
throw new Error('OpenAI-compatible provider requires a custom base URL');
}
return createOpenAI({
name: 'openai-compatible',
apiKey: profile.apiKey,
baseURL,
fetch,
});
case 'xai':
return createXai({ apiKey: profile.apiKey, baseURL, fetch });
case 'groq':
return createGroq({ apiKey: profile.apiKey, baseURL, fetch });
case 'zhipu':
return createZhipu({ apiKey: profile.apiKey, baseURL, fetch });
case 'deepseek':
return createDeepSeek({ apiKey: profile.apiKey, baseURL, fetch });
case 'mistral':
return createMistral({ apiKey: profile.apiKey, baseURL, fetch });
default: {
const _exhaustive: never = profile.providerType;
throw new Error(`Unknown provider type: ${_exhaustive}`);

View file

@ -4,7 +4,7 @@
* Designed for extensibility to support multiple TTS providers.
*/
import { OPENROUTER_API_URL } from '../sdk/providers/defaults';
import { PROVIDERS } from '../sdk/providers/config';
import type { APIProfile } from "$lib/types";
import { corsFetch } from "$lib/services/discovery/utils";
@ -319,7 +319,7 @@ export class OpenAICompatibleTTSProvider extends TTSProvider {
private getEndpoint(): string {
// If no custom endpoint, use OpenRouter default
if (!this.settings.endpoint) {
return `${OPENROUTER_API_URL}/audio/speech`;
return `${PROVIDERS.openrouter.baseUrl}/audio/speech`;
}
// Ensure endpoint ends with /audio/speech
const url = this.settings.endpoint.replace(/\/$/, "");

View file

@ -5,7 +5,7 @@ import {
getDefaultAdvancedSettings,
getDefaultAdvancedSettingsForProvider,
} from '$lib/services/ai/wizard/ScenarioService';
import { PROVIDER_DEFAULTS, OPENROUTER_API_URL } from '$lib/services/ai/sdk/providers/defaults';
import { PROVIDERS } from '$lib/services/ai/sdk/providers/config';
import { promptService, type PromptSettings, getDefaultPromptSettings } from '$lib/services/prompts';
import type { ReasoningEffort } from '$lib/types';
import { ui } from '$lib/stores/ui.svelte';
@ -19,9 +19,6 @@ export type ProviderPreset = 'openrouter' | 'nanogpt' | 'custom';
export const DEFAULT_OPENROUTER_PROFILE_ID = 'default-openrouter-profile';
export const DEFAULT_NANOGPT_PROFILE_ID = 'default-nanogpt-profile';
// Provider URLs
export const NANOGPT_API_URL = 'https://nano-gpt.com/api/v1';
// NOTE: Default story prompts are now in the centralized prompt system at
// src/lib/services/prompts/definitions.ts (template ids: 'adventure', 'creative-writing')
// The prompt fields in StoryGenerationSettings are kept for backwards compatibility
@ -919,11 +916,11 @@ export function getDefaultSystemServicesSettingsForProvider(provider: ProviderTy
/**
* Get default generation presets (Agent Profiles) for a specific provider.
* Uses PROVIDER_DEFAULTS from the SDK for model and settings defaults.
* Uses PROVIDERS from the SDK for model and settings defaults.
* @param provider - The provider type to get defaults for
*/
export function getDefaultGenerationPresetsForProvider(provider: ProviderType): GenerationPreset[] {
const defaults = PROVIDER_DEFAULTS[provider];
const defaults = PROVIDERS[provider];
return [
{
@ -931,10 +928,10 @@ export function getDefaultGenerationPresetsForProvider(provider: ProviderType):
name: 'Classification',
description: 'World state, lorebook parsing, entity extraction',
profileId: null,
model: defaults.classification.model,
temperature: defaults.classification.temperature,
maxTokens: defaults.classification.maxTokens,
reasoningEffort: defaults.classification.reasoningEffort,
model: defaults.services.classification.model,
temperature: defaults.services.classification.temperature,
maxTokens: defaults.services.classification.maxTokens,
reasoningEffort: defaults.services.classification.reasoningEffort,
manualBody: ''
},
{
@ -942,10 +939,10 @@ export function getDefaultGenerationPresetsForProvider(provider: ProviderType):
name: 'Memory & Context',
description: 'Chapter analysis, timeline, context retrieval',
profileId: null,
model: defaults.memory.model,
temperature: defaults.memory.temperature,
maxTokens: defaults.memory.maxTokens,
reasoningEffort: defaults.memory.reasoningEffort,
model: defaults.services.memory.model,
temperature: defaults.services.memory.temperature,
maxTokens: defaults.services.memory.maxTokens,
reasoningEffort: defaults.services.memory.reasoningEffort,
manualBody: ''
},
{
@ -953,10 +950,10 @@ export function getDefaultGenerationPresetsForProvider(provider: ProviderType):
name: 'Suggestions',
description: 'Plot suggestions, action choices, style review',
profileId: null,
model: defaults.suggestions.model,
temperature: defaults.suggestions.temperature,
maxTokens: defaults.suggestions.maxTokens,
reasoningEffort: defaults.suggestions.reasoningEffort,
model: defaults.services.suggestions.model,
temperature: defaults.services.suggestions.temperature,
maxTokens: defaults.services.suggestions.maxTokens,
reasoningEffort: defaults.services.suggestions.reasoningEffort,
manualBody: ''
},
{
@ -964,10 +961,10 @@ export function getDefaultGenerationPresetsForProvider(provider: ProviderType):
name: 'Agentic',
description: 'Autonomous lore management and retrieval',
profileId: null,
model: defaults.agentic.model,
temperature: defaults.agentic.temperature,
maxTokens: defaults.agentic.maxTokens,
reasoningEffort: defaults.agentic.reasoningEffort,
model: defaults.services.agentic.model,
temperature: defaults.services.agentic.temperature,
maxTokens: defaults.services.agentic.maxTokens,
reasoningEffort: defaults.services.agentic.reasoningEffort,
manualBody: ''
},
{
@ -975,10 +972,10 @@ export function getDefaultGenerationPresetsForProvider(provider: ProviderType):
name: 'Story Wizard',
description: 'Story setup, character and setting generation',
profileId: null,
model: defaults.wizard.model,
temperature: defaults.wizard.temperature,
maxTokens: defaults.wizard.maxTokens,
reasoningEffort: defaults.wizard.reasoningEffort,
model: defaults.services.wizard.model,
temperature: defaults.services.wizard.temperature,
maxTokens: defaults.services.wizard.maxTokens,
reasoningEffort: defaults.services.wizard.reasoningEffort,
manualBody: ''
},
{
@ -986,10 +983,10 @@ export function getDefaultGenerationPresetsForProvider(provider: ProviderType):
name: 'Translation',
description: 'Text translation between languages',
profileId: null,
model: defaults.translation.model,
temperature: defaults.translation.temperature,
maxTokens: defaults.translation.maxTokens,
reasoningEffort: defaults.translation.reasoningEffort,
model: defaults.services.translation.model,
temperature: defaults.services.translation.temperature,
maxTokens: defaults.services.translation.maxTokens,
reasoningEffort: defaults.services.translation.reasoningEffort,
manualBody: ''
}
];
@ -1018,7 +1015,7 @@ class SettingsStore {
apiSettings = $state<APISettings>({
openaiApiKey: null,
openaiApiURL: OPENROUTER_API_URL,
openaiApiURL: PROVIDERS.openrouter.baseUrl,
profiles: [],
activeProfileId: null,
mainNarrativeProfileId: DEFAULT_OPENROUTER_PROFILE_ID,
@ -1177,7 +1174,7 @@ class SettingsStore {
try {
// Load API settings
const apiURL = await database.getSetting('openai_api_url') ?? OPENROUTER_API_URL; //Default to OpenRouter.
const apiURL = await database.getSetting('openai_api_url') ?? PROVIDERS.openrouter.baseUrl; //Default to OpenRouter.
// Load API key - check multiple locations for migration
// Must handle empty strings explicitly since ?? only checks for null/undefined
@ -1507,7 +1504,7 @@ class SettingsStore {
// Only ensure default profile and migrate for existing users (who have completed first run)
// New users will get their profile created in initializeWithProvider after selecting a provider
if (this.firstRunComplete) {
const isOpenRouterUrl = apiURL === OPENROUTER_API_URL;
const isOpenRouterUrl = apiURL === PROVIDERS.openrouter.baseUrl;
const isOpenRouterKey = !!apiKey && apiKey.startsWith('sk-or-');
const shouldEnsureOpenRouterProfile = this.providerPreset === 'openrouter' || isOpenRouterUrl || isOpenRouterKey;
const openRouterApiKey = (isOpenRouterUrl || isOpenRouterKey) ? apiKey : null;
@ -1716,12 +1713,12 @@ class SettingsStore {
const defaultProfile = this.getProfile(defaultProfileId);
if (defaultProfile) {
this.apiSettings.activeProfileId = defaultProfileId;
this.apiSettings.openaiApiURL = defaultProfile.baseUrl ?? OPENROUTER_API_URL;
this.apiSettings.openaiApiURL = defaultProfile.baseUrl ?? PROVIDERS.openrouter.baseUrl;
this.apiSettings.openaiApiKey = defaultProfile.apiKey;
} else if (this.apiSettings.profiles.length > 0) {
const fallbackProfile = this.apiSettings.profiles[0];
this.apiSettings.activeProfileId = fallbackProfile.id;
this.apiSettings.openaiApiURL = fallbackProfile.baseUrl ?? OPENROUTER_API_URL;
this.apiSettings.openaiApiURL = fallbackProfile.baseUrl ?? PROVIDERS.openrouter.baseUrl;
this.apiSettings.openaiApiKey = fallbackProfile.apiKey;
} else {
this.apiSettings.activeProfileId = null;
@ -1811,7 +1808,7 @@ class SettingsStore {
if (defaultProfile) {
return {
...this.apiSettings,
openaiApiURL: defaultProfile.baseUrl ?? OPENROUTER_API_URL,
openaiApiURL: defaultProfile.baseUrl ?? PROVIDERS.openrouter.baseUrl,
openaiApiKey: defaultProfile.apiKey,
};
}
@ -1821,7 +1818,7 @@ class SettingsStore {
return {
...this.apiSettings,
openaiApiURL: profile.baseUrl ?? OPENROUTER_API_URL,
openaiApiURL: profile.baseUrl ?? PROVIDERS.openrouter.baseUrl,
openaiApiKey: profile.apiKey,
};
}
@ -1957,7 +1954,7 @@ class SettingsStore {
id: DEFAULT_OPENROUTER_PROFILE_ID,
name: 'OpenRouter',
providerType: 'openrouter',
baseUrl: OPENROUTER_API_URL,
baseUrl: PROVIDERS.openrouter.baseUrl,
apiKey: existingApiKey || '', // Migrate existing key if present
customModels: allModels, // Include all models in use plus defaults
fetchedModels: [], // Will be populated when user fetches from API
@ -1973,7 +1970,7 @@ class SettingsStore {
if (!this.apiSettings.activeProfileId) {
this.apiSettings.activeProfileId = DEFAULT_OPENROUTER_PROFILE_ID;
// Also set the current URL/key to match the profile (legacy fields)
this.apiSettings.openaiApiURL = defaultProfile.baseUrl ?? OPENROUTER_API_URL;
this.apiSettings.openaiApiURL = defaultProfile.baseUrl ?? PROVIDERS.openrouter.baseUrl;
this.apiSettings.openaiApiKey = defaultProfile.apiKey;
}
@ -2247,7 +2244,7 @@ class SettingsStore {
}
// Fall back to legacy check for pre-profile installations
return (!this.apiSettings.openaiApiKey && this.apiSettings.openaiApiURL === OPENROUTER_API_URL);
return (!this.apiSettings.openaiApiKey && this.apiSettings.openaiApiURL === PROVIDERS.openrouter.baseUrl);
}
// Wizard settings methods
@ -2492,16 +2489,16 @@ class SettingsStore {
*/
async resetAllSettings(preserveApiSettings = true) {
const provider = this.getDefaultProviderType();
const defaults = PROVIDER_DEFAULTS[provider];
const defaults = PROVIDERS[provider];
const apiKey = preserveApiSettings ? this.apiSettings.openaiApiKey : null;
const apiURL = preserveApiSettings ? this.apiSettings.openaiApiURL : (defaults.baseUrl || OPENROUTER_API_URL);
const apiURL = preserveApiSettings ? this.apiSettings.openaiApiURL : (defaults.baseUrl || PROVIDERS.openrouter.baseUrl);
const profiles = preserveApiSettings ? this.apiSettings.profiles : [];
const activeProfileId = preserveApiSettings ? this.apiSettings.activeProfileId : null;
const mainNarrativeProfileId = preserveApiSettings ? this.apiSettings.mainNarrativeProfileId : null;
const defaultNarrativeModel = defaults.narrative.model;
const defaultReasoningEffort = defaults.narrative.reasoningEffort;
const defaultNarrativeModel = defaults.services.narrative.model;
const defaultReasoningEffort = defaults.services.narrative.reasoningEffort;
// Reset API settings (except URL/key/profiles if preserving)
this.apiSettings = {
@ -2603,7 +2600,7 @@ class SettingsStore {
* This sets up the default profile and all settings based on the provider.
*/
async initializeWithProvider(provider: ProviderType, apiKey: string) {
const defaults = PROVIDER_DEFAULTS[provider];
const defaults = PROVIDERS[provider];
// Set the provider preset
this.providerPreset = provider;
@ -2611,7 +2608,7 @@ class SettingsStore {
// Create a unique profile ID
const defaultProfileId = `default-${provider}-profile`;
const defaultApiURL = defaults.baseUrl || OPENROUTER_API_URL;
const defaultApiURL = defaults.baseUrl || PROVIDERS.openrouter.baseUrl;
const defaultProfile: APIProfile = {
id: defaultProfileId,
@ -2641,10 +2638,10 @@ class SettingsStore {
this.apiSettings.openaiApiKey = apiKey;
// Set provider-specific defaults
this.apiSettings.defaultModel = defaults.narrative.model;
this.apiSettings.temperature = defaults.narrative.temperature;
this.apiSettings.maxTokens = defaults.narrative.maxTokens;
this.apiSettings.reasoningEffort = defaults.narrative.reasoningEffort;
this.apiSettings.defaultModel = defaults.services.narrative.model;
this.apiSettings.temperature = defaults.services.narrative.temperature;
this.apiSettings.maxTokens = defaults.services.narrative.maxTokens;
this.apiSettings.reasoningEffort = defaults.services.narrative.reasoningEffort;
this.apiSettings.manualBody = '';
this.apiSettings.enableThinking = false;
await database.setSetting('default_model', this.apiSettings.defaultModel);

View file

@ -629,13 +629,23 @@ export interface UIState {
// Provider types matching Vercel AI SDK providers
export type ProviderType =
| 'openrouter' // @openrouter/ai-sdk-provider
| 'openai' // @ai-sdk/openai
| 'anthropic' // @ai-sdk/anthropic
| 'google' // @ai-sdk/google
| 'nanogpt' // OpenAI-compatible at nano-gpt.com
| 'chutes' // @chutes-ai/ai-sdk-provider
| 'pollinations'; // ai-sdk-pollinations
| 'openrouter' // @openrouter/ai-sdk-provider
| 'nanogpt' // OpenAI-compatible at nano-gpt.com
| 'chutes' // @chutes-ai/ai-sdk-provider
| 'pollinations' // ai-sdk-pollinations
| 'ollama' // ollama-ai-provider (local)
| 'lmstudio' // @ai-sdk/openai (local, default localhost:1234)
| 'llamacpp' // @ai-sdk/openai (local, default localhost:8080)
| 'nvidia-nim' // @ai-sdk/openai (NVIDIA NIM)
| 'openai-compatible' // @ai-sdk/openai (requires custom baseUrl)
| 'openai' // @ai-sdk/openai
| 'anthropic' // @ai-sdk/anthropic
| 'google' // @ai-sdk/google
| 'xai' // @ai-sdk/xai (Grok)
| 'groq' // @ai-sdk/groq
| 'zhipu' // zhipu-ai-provider (Z.AI/GLM)
| 'deepseek' // @ai-sdk/deepseek
| 'mistral'; // @ai-sdk/mistral
// API Profile for saving OpenAI-compatible endpoint configurations
export interface APIProfile {