Add OpenRouter provider with DeepSeek support - best value for translation (.14/M tokens)

This commit is contained in:
2025-11-30 22:10:34 +01:00
parent b65e683d32
commit 3346817a8a
7 changed files with 601 additions and 8 deletions

View File

@@ -16,6 +16,7 @@ export default function Home() {
const { settings } = useTranslationStore();
const providerNames: Record<string, string> = {
openrouter: "OpenRouter",
google: "Google Translate",
ollama: "Ollama",
deepl: "DeepL",

View File

@@ -7,10 +7,10 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { useTranslationStore, webllmModels, openaiModels } from "@/lib/store";
import { useTranslationStore, webllmModels, openaiModels, openrouterModels } from "@/lib/store";
import { providers, testOpenAIConnection, testOllamaConnection, getOllamaModels, type OllamaModel } from "@/lib/api";
import { useWebLLM } from "@/lib/webllm";
import { Save, Loader2, Cloud, Check, ExternalLink, Wifi, CheckCircle, XCircle, Download, Trash2, Cpu, Server, RefreshCw } from "lucide-react";
import { Save, Loader2, Cloud, Check, ExternalLink, Wifi, CheckCircle, XCircle, Download, Trash2, Cpu, Server, RefreshCw, Zap } from "lucide-react";
import {
Select,
SelectContent,
@@ -30,6 +30,8 @@ export default function TranslationServicesPage() {
const [deeplApiKey, setDeeplApiKey] = useState(settings.deeplApiKey);
const [openaiApiKey, setOpenaiApiKey] = useState(settings.openaiApiKey);
const [openaiModel, setOpenaiModel] = useState(settings.openaiModel);
const [openrouterApiKey, setOpenrouterApiKey] = useState(settings.openrouterApiKey);
const [openrouterModel, setOpenrouterModel] = useState(settings.openrouterModel);
const [libreUrl, setLibreUrl] = useState(settings.libreTranslateUrl);
const [webllmModel, setWebllmModel] = useState(settings.webllmModel);
@@ -44,6 +46,10 @@ export default function TranslationServicesPage() {
// OpenAI connection test state
const [openaiTestStatus, setOpenaiTestStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
const [openaiTestMessage, setOpenaiTestMessage] = useState("");
// OpenRouter connection test state
const [openrouterTestStatus, setOpenrouterTestStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
const [openrouterTestMessage, setOpenrouterTestMessage] = useState("");
// WebLLM hook
const webllm = useWebLLM();
@@ -54,6 +60,8 @@ export default function TranslationServicesPage() {
setDeeplApiKey(settings.deeplApiKey);
setOpenaiApiKey(settings.openaiApiKey);
setOpenaiModel(settings.openaiModel);
setOpenrouterApiKey(settings.openrouterApiKey);
setOpenrouterModel(settings.openrouterModel);
setLibreUrl(settings.libreTranslateUrl);
setWebllmModel(settings.webllmModel);
setOllamaUrl(settings.ollamaUrl);
@@ -124,6 +132,31 @@ export default function TranslationServicesPage() {
setOpenaiTestMessage("Connection test failed");
}
};
// Test OpenRouter connection
const testOpenRouterConnection = async () => {
if (!openrouterApiKey) {
setOpenrouterTestStatus("error");
setOpenrouterTestMessage("API key required");
return;
}
setOpenrouterTestStatus("testing");
try {
const response = await fetch("https://openrouter.ai/api/v1/models", {
headers: { Authorization: `Bearer ${openrouterApiKey}` }
});
if (response.ok) {
setOpenrouterTestStatus("success");
setOpenrouterTestMessage("Connected successfully!");
} else {
setOpenrouterTestStatus("error");
setOpenrouterTestMessage("Invalid API key");
}
} catch {
setOpenrouterTestStatus("error");
setOpenrouterTestMessage("Connection test failed");
}
};
const handleSave = async () => {
setIsSaving(true);
@@ -134,6 +167,8 @@ export default function TranslationServicesPage() {
deeplApiKey,
openaiApiKey,
openaiModel,
openrouterApiKey,
openrouterModel,
libreTranslateUrl: libreUrl,
webllmModel,
ollamaUrl,
@@ -553,6 +588,125 @@ export default function TranslationServicesPage() {
</Card>
)}
{/* OpenRouter Settings */}
{selectedProvider === "openrouter" && (
<Card className="border-zinc-800 bg-zinc-900/50 border-teal-500/30">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Zap className="h-6 w-6 text-teal-400" />
<div>
<CardTitle className="text-white">OpenRouter Settings</CardTitle>
<CardDescription>
Access DeepSeek, Mistral, Llama & more - Best value for translation
</CardDescription>
</div>
</div>
{openrouterTestStatus !== "idle" && openrouterTestStatus !== "testing" && (
<Badge
variant="outline"
className={
openrouterTestStatus === "success"
? "border-green-500 text-green-400"
: "border-red-500 text-red-400"
}
>
{openrouterTestStatus === "success" && <CheckCircle className="h-3 w-3 mr-1" />}
{openrouterTestStatus === "error" && <XCircle className="h-3 w-3 mr-1" />}
{openrouterTestStatus === "success" ? "Connected" : "Error"}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="openrouter-key" className="text-zinc-300">
API Key
</Label>
<div className="flex gap-2">
<Input
id="openrouter-key"
type="password"
value={openrouterApiKey}
onChange={(e) => setOpenrouterApiKey(e.target.value)}
onKeyDown={(e) => e.stopPropagation()}
placeholder="sk-or-..."
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
/>
<Button
variant="outline"
onClick={testOpenRouterConnection}
disabled={openrouterTestStatus === "testing"}
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800"
>
{openrouterTestStatus === "testing" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Wifi className="h-4 w-4" />
)}
</Button>
</div>
{openrouterTestMessage && (
<p className={`text-xs ${openrouterTestStatus === "success" ? "text-green-400" : "text-red-400"}`}>
{openrouterTestMessage}
</p>
)}
<p className="text-xs text-zinc-500">
Get your free API key from{" "}
<a
href="https://openrouter.ai/keys"
target="_blank"
rel="noopener noreferrer"
className="text-teal-400 hover:underline"
>
openrouter.ai/keys
</a>
</p>
</div>
<div className="space-y-2">
<Label htmlFor="openrouter-model" className="text-zinc-300">
Model
</Label>
<Select
value={openrouterModel}
onValueChange={setOpenrouterModel}
>
<SelectTrigger
id="openrouter-model"
className="bg-zinc-800 border-zinc-700 text-white"
>
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent className="bg-zinc-800 border-zinc-700">
{openrouterModels.map((model) => (
<SelectItem
key={model.id}
value={model.id}
className="text-white hover:bg-zinc-700"
>
<span className="flex items-center justify-between gap-4">
<span>{model.name}</span>
<Badge variant="outline" className="border-zinc-600 text-zinc-400 text-xs ml-2">
{model.description.split(' - ')[1]}
</Badge>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-zinc-500">
DeepSeek Chat offers the best quality/price ratio for translations
</p>
</div>
<div className="rounded-lg bg-teal-500/10 border border-teal-500/30 p-3">
<p className="text-sm text-teal-300">
💡 <strong>Recommended:</strong> DeepSeek Chat at $0.14/M tokens translates 200 pages for ~$0.50
</p>
</div>
</CardContent>
</Card>
)}
{/* OpenAI Settings */}
{selectedProvider === "openai" && (
<Card className="border-zinc-800 bg-zinc-900/50">

View File

@@ -24,7 +24,7 @@ const fileIcons: Record<string, React.ElementType> = {
ppt: Presentation,
};
type ProviderType = "google" | "ollama" | "deepl" | "libre" | "webllm" | "openai";
type ProviderType = "google" | "ollama" | "deepl" | "libre" | "webllm" | "openai" | "openrouter";
export function FileUploader() {
const { settings, isTranslating, progress, setTranslating, setProgress } = useTranslationStore();
@@ -220,6 +220,8 @@ export function FileUploader() {
libreUrl: settings.libreTranslateUrl,
openaiApiKey: settings.openaiApiKey,
openaiModel: settings.openaiModel,
openrouterApiKey: settings.openrouterApiKey,
openrouterModel: settings.openrouterModel,
});
clearInterval(progressInterval);