Add OpenRouter provider with DeepSeek support - best value for translation (.14/M tokens)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user