mirror of
https://github.com/AventurasTeam/Aventuras.git
synced 2026-04-28 03:40:11 +00:00
added providers with easy provider configurations
This commit is contained in:
parent
efbb6830e1
commit
d092bfecbb
18 changed files with 911 additions and 901 deletions
202
package-lock.json
generated
202
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
391
src/lib/services/ai/sdk/providers/config.ts
Normal file
391
src/lib/services/ai/sdk/providers/config.ts
Normal 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,
|
||||
}));
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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(/\/$/, "");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue