mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix: routing strategy not persisted after refresh (#134)
The Zod updateSettingsSchema was missing fallbackStrategy, wildcardAliases, and stickyRoundRobinLimit fields. Since .passthrough() was removed in a previous cleanup, these unknown keys were silently stripped during validation, so the PATCH /api/settings call never actually persisted these values.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState } from "react";
|
||||
import { SegmentedControl } from "@/shared/components";
|
||||
import PlaygroundMode from "./components/PlaygroundMode";
|
||||
@@ -7,26 +9,21 @@ import ChatTesterMode from "./components/ChatTesterMode";
|
||||
import TestBenchMode from "./components/TestBenchMode";
|
||||
import LiveMonitorMode from "./components/LiveMonitorMode";
|
||||
|
||||
const MODES = [
|
||||
{ value: "playground", label: "Playground", icon: "code" },
|
||||
{ value: "chat-tester", label: "Chat Tester", icon: "chat" },
|
||||
{ value: "test-bench", label: "Test Bench", icon: "science" },
|
||||
{ value: "live-monitor", label: "Live Monitor", icon: "monitoring" },
|
||||
];
|
||||
|
||||
const MODE_DESCRIPTIONS: Record<string, string> = {
|
||||
playground:
|
||||
"Paste any API request body and see how OmniRoute translates it between provider formats (OpenAI ↔ Claude ↔ Gemini ↔ Responses API)",
|
||||
"chat-tester":
|
||||
"Send real chat requests through OmniRoute and see the full round-trip: your input, the translated request, the provider response, and the translated output",
|
||||
"test-bench":
|
||||
"Define multiple test cases with different inputs and expected outputs, run them all at once, and compare results across providers and models",
|
||||
"live-monitor":
|
||||
"Watch incoming requests in real-time as they flow through OmniRoute — see format translations happening live and identify issues instantly",
|
||||
};
|
||||
|
||||
export default function TranslatorPageClient() {
|
||||
const t = useTranslations("translator");
|
||||
const [mode, setMode] = useState("playground");
|
||||
const modes = [
|
||||
{ value: "playground", label: t("playground"), icon: "code" },
|
||||
{ value: "chat-tester", label: t("chatTester"), icon: "chat" },
|
||||
{ value: "test-bench", label: t("testBench"), icon: "science" },
|
||||
{ value: "live-monitor", label: t("liveMonitor"), icon: "monitoring" },
|
||||
];
|
||||
const modeDescriptions: Record<string, string> = {
|
||||
playground: t("modeDescriptionPlayground"),
|
||||
"chat-tester": t("modeDescriptionChatTester"),
|
||||
"test-bench": t("modeDescriptionTestBench"),
|
||||
"live-monitor": t("modeDescriptionLiveMonitor"),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-6">
|
||||
@@ -35,14 +32,13 @@ export default function TranslatorPageClient() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-main flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[28px]">translate</span>
|
||||
Translator Playground
|
||||
{t("playgroundTitle")}
|
||||
</h1>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
{MODE_DESCRIPTIONS[mode] ||
|
||||
"Debug, test, and visualize how OmniRoute translates API requests between providers"}
|
||||
{modeDescriptions[mode] || t("modeDescriptionFallback")}
|
||||
</p>
|
||||
</div>
|
||||
<SegmentedControl options={MODES} value={mode} onChange={setMode} size="md" />
|
||||
<SegmentedControl options={modes} value={mode} onChange={setMode} size="md" />
|
||||
</div>
|
||||
|
||||
{/* Mode Content */}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { FORMAT_META } from "../exampleTemplates";
|
||||
*/
|
||||
export default function LiveMonitorMode() {
|
||||
const t = useTranslations("translator");
|
||||
const tc = useTranslations("common");
|
||||
const [events, setEvents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
@@ -60,21 +61,25 @@ export default function LiveMonitorMode() {
|
||||
<div>
|
||||
<p className="font-medium text-text-main mb-0.5">{t("realtime")}</p>
|
||||
<p>
|
||||
Shows translation events as API calls flow through OmniRoute. Events come from the
|
||||
in-memory buffer (resets on restart). Use{" "}
|
||||
{t("liveMonitorDescription")}{" "}
|
||||
<strong className="text-text-main">{t("chatTester")}</strong>,{" "}
|
||||
<strong className="text-text-main">{t("testBench")}</strong>, or external API calls to
|
||||
generate events.
|
||||
<strong className="text-text-main">{t("testBench")}</strong>, {t("or")}{" "}
|
||||
{t("externalApiCalls").toLowerCase()} {t("toGenerateEvents")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<StatCard icon="translate" label="Total Translations" value={events.length} color="blue" />
|
||||
<StatCard icon="check_circle" label="Successful" value={successCount} color="green" />
|
||||
<StatCard icon="error" label="Errors" value={errorCount} color="red" />
|
||||
<StatCard icon="speed" label="Avg Latency" value={`${avgLatency}ms`} color="purple" />
|
||||
<StatCard
|
||||
icon="translate"
|
||||
label={t("totalTranslations")}
|
||||
value={events.length}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard icon="check_circle" label={t("successful")} value={successCount} color="green" />
|
||||
<StatCard icon="error" label={t("errors")} value={errorCount} color="red" />
|
||||
<StatCard icon="speed" label={t("avgLatency")} value={`${avgLatency}ms`} color="purple" />
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
@@ -90,7 +95,7 @@ export default function LiveMonitorMode() {
|
||||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||
className="text-sm text-text-main hover:text-primary transition-colors"
|
||||
>
|
||||
{autoRefresh ? "Live — Auto-refreshing" : "Paused"}
|
||||
{autoRefresh ? t("liveAutoRefreshing") : t("paused")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@@ -98,7 +103,7 @@ export default function LiveMonitorMode() {
|
||||
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">refresh</span>
|
||||
Refresh
|
||||
{tc("refresh")}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -111,7 +116,7 @@ export default function LiveMonitorMode() {
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12 text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin mr-2">progress_activity</span>
|
||||
Loading...
|
||||
{tc("loading")}
|
||||
</div>
|
||||
) : events.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
|
||||
@@ -120,39 +125,36 @@ export default function LiveMonitorMode() {
|
||||
</span>
|
||||
<p className="text-sm font-medium mb-1">{t("noTranslations")}</p>
|
||||
<p className="text-xs text-center max-w-sm">
|
||||
Translation events appear here as requests flow through OmniRoute. Use any of these
|
||||
methods to generate events:
|
||||
{t("eventsAppearHint")}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3 text-xs">
|
||||
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
|
||||
Chat Tester tab
|
||||
{t("chatTesterTab")}
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
|
||||
Test Bench tab
|
||||
{t("testBenchTab")}
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
|
||||
External API calls
|
||||
{t("externalApiCalls")}
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
|
||||
IDE/CLI integrations
|
||||
{t("ideCliIntegrations")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] mt-3 text-text-muted/70">
|
||||
Note: Events are stored in-memory and reset when the server restarts.
|
||||
</p>
|
||||
<p className="text-[10px] mt-3 text-text-muted/70">{t("inMemoryNote")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-xs text-text-muted border-b border-border">
|
||||
<th className="pb-2 pr-4">{t(">time</")}</th>
|
||||
<th className="pb-2 pr-4">{t(">source</")}</th>
|
||||
<th className="pb-2 pr-4">{t("time")}</th>
|
||||
<th className="pb-2 pr-4">{t("source")}</th>
|
||||
<th className="pb-2 pr-4"></th>
|
||||
<th className="pb-2 pr-4">{t(">target</")}</th>
|
||||
<th className="pb-2 pr-4">{t(">model</")}</th>
|
||||
<th className="pb-2 pr-4">{t(">status</")}</th>
|
||||
<th className="pb-2 text-right">{t(">latency</")}</th>
|
||||
<th className="pb-2 pr-4">{t("target")}</th>
|
||||
<th className="pb-2 pr-4">{t("model")}</th>
|
||||
<th className="pb-2 pr-4">{t("status")}</th>
|
||||
<th className="pb-2 text-right">{t("latency")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -195,11 +197,11 @@ export default function LiveMonitorMode() {
|
||||
<td className="py-2 pr-4">
|
||||
{event.status === "success" ? (
|
||||
<Badge variant="success" size="sm" dot>
|
||||
OK
|
||||
{t("ok")}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="error" size="sm" dot>
|
||||
{event.statusCode || "ERR"}
|
||||
{event.statusCode || t("errorShort")}
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -11,6 +11,7 @@ const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||
|
||||
export default function PlaygroundMode() {
|
||||
const t = useTranslations("translator");
|
||||
const tc = useTranslations("common");
|
||||
const [sourceFormat, setSourceFormat] = useState("claude");
|
||||
const [targetFormat, setTargetFormat] = useState("openai");
|
||||
const [inputContent, setInputContent] = useState("");
|
||||
@@ -118,11 +119,7 @@ export default function PlaygroundMode() {
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-text-main mb-0.5">{t("formatConverter")}</p>
|
||||
<p>
|
||||
Paste or type a JSON request body. The translator will auto-detect the source format and
|
||||
convert it to the target format. Use this to debug how OmniRoute translates requests
|
||||
between formats (OpenAI ↔ Claude ↔ Gemini ↔ Responses API).
|
||||
</p>
|
||||
<p>{t("formatConverterDescription")}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Format Controls Bar */}
|
||||
@@ -131,7 +128,7 @@ export default function PlaygroundMode() {
|
||||
{/* Source Format */}
|
||||
<div className="flex-1 w-full">
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
|
||||
Source Format
|
||||
{t("source")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`material-symbols-outlined text-[20px] text-${srcMeta.color}-500`}>
|
||||
@@ -148,7 +145,7 @@ export default function PlaygroundMode() {
|
||||
/>
|
||||
{detectedFormat && (
|
||||
<Badge variant="primary" size="sm" icon="auto_awesome">
|
||||
Auto
|
||||
{t("auto")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -158,7 +155,7 @@ export default function PlaygroundMode() {
|
||||
<button
|
||||
onClick={handleSwapFormats}
|
||||
className="p-2 rounded-full hover:bg-primary/10 text-text-muted hover:text-primary transition-all mt-4 sm:mt-5"
|
||||
title="Swap formats"
|
||||
title={t("swapFormats")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[24px]">swap_horiz</span>
|
||||
</button>
|
||||
@@ -166,7 +163,7 @@ export default function PlaygroundMode() {
|
||||
{/* Target Format */}
|
||||
<div className="flex-1 w-full">
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
|
||||
Target Format
|
||||
{t("target")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`material-symbols-outlined text-[20px] text-${tgtMeta.color}-500`}>
|
||||
@@ -190,7 +187,7 @@ export default function PlaygroundMode() {
|
||||
disabled={!inputContent.trim() || translating}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Translate
|
||||
{t("translateAction")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +201,7 @@ export default function PlaygroundMode() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[18px] text-text-muted">input</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t(">input</")}</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t("input")}</h3>
|
||||
{detectedFormat && (
|
||||
<Badge variant="info" size="sm" dot>
|
||||
{FORMAT_META[detectedFormat]?.label || detectedFormat}
|
||||
@@ -220,7 +217,7 @@ export default function PlaygroundMode() {
|
||||
<button
|
||||
onClick={() => handleCopy(inputContent)}
|
||||
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
|
||||
title="Copy"
|
||||
title={tc("copy")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">content_copy</span>
|
||||
</button>
|
||||
@@ -232,7 +229,7 @@ export default function PlaygroundMode() {
|
||||
setActiveTemplate(null);
|
||||
}}
|
||||
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
|
||||
title="Clear"
|
||||
title={t("clear")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">delete</span>
|
||||
</button>
|
||||
@@ -253,7 +250,7 @@ export default function PlaygroundMode() {
|
||||
wordWrap: "on",
|
||||
automaticLayout: true,
|
||||
formatOnPaste: true,
|
||||
placeholder: "Paste a request body here or select a template below...",
|
||||
placeholder: t("inputPlaceholder"),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -268,7 +265,7 @@ export default function PlaygroundMode() {
|
||||
<span className="material-symbols-outlined text-[18px] text-text-muted">
|
||||
output
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t(">output</")}</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t("output")}</h3>
|
||||
{outputContent && (
|
||||
<Badge variant="success" size="sm" dot>
|
||||
{FORMAT_META[targetFormat]?.label || targetFormat}
|
||||
@@ -279,7 +276,7 @@ export default function PlaygroundMode() {
|
||||
<button
|
||||
onClick={() => handleCopy(outputContent)}
|
||||
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
|
||||
title="Copy"
|
||||
title={tc("copy")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">content_copy</span>
|
||||
</button>
|
||||
@@ -313,8 +310,8 @@ export default function PlaygroundMode() {
|
||||
<span className="material-symbols-outlined text-[18px] text-primary">
|
||||
library_books
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">Example Templates</h3>
|
||||
<span className="text-xs text-text-muted">— Click to load</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t("exampleTemplates")}</h3>
|
||||
<span className="text-xs text-text-muted">{t("exampleTemplatesHint")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
|
||||
{EXAMPLE_TEMPLATES.map((template) => (
|
||||
@@ -342,11 +339,9 @@ export default function PlaygroundMode() {
|
||||
{activeTemplate && (
|
||||
<div className="flex items-center gap-2 text-xs text-text-muted">
|
||||
<span className="material-symbols-outlined text-[14px]">info</span>
|
||||
Template loads the request in{" "}
|
||||
<strong className="text-text-main">
|
||||
{FORMAT_META[sourceFormat]?.label || sourceFormat}
|
||||
</strong>{" "}
|
||||
format. Change Source Format to load in a different format.
|
||||
{t("templateLoadHint", {
|
||||
format: FORMAT_META[sourceFormat]?.label || sourceFormat,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import TranslatorPageClient from "./TranslatorPageClient";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export const metadata = {
|
||||
title: "Translator Playground | OmniRoute",
|
||||
description: "Debug, test, and visualize API format translations between providers",
|
||||
};
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations("translator");
|
||||
return {
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
};
|
||||
}
|
||||
|
||||
export default function TranslatorPage() {
|
||||
return <TranslatorPageClient />;
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function BudgetTab() {
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="text-sm text-text-muted mb-1 block">{t(">apiKey</")}</label>
|
||||
<label className="text-sm text-text-muted mb-1 block">{t("apiKey")}</label>
|
||||
<select
|
||||
value={selectedKey || ""}
|
||||
onChange={(e) => setSelectedKey(e.target.value)}
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function BudgetTelemetryCards() {
|
||||
{cache ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">{t(">entries</")}</span>
|
||||
<span className="text-text-muted">{t("entries")}</span>
|
||||
<span className="font-mono">
|
||||
{cache.size}/{cache.maxSize}
|
||||
</span>
|
||||
|
||||
@@ -292,7 +292,7 @@ export default function EvalsTab() {
|
||||
<div className="w-10 h-10 rounded-full bg-violet-500/20 flex items-center justify-center mb-3">
|
||||
<span className="text-lg font-bold text-violet-400">1</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">define</")}</h4>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t("define")}</h4>
|
||||
<p className="text-xs text-text-muted">
|
||||
Create test cases with input prompts and expected output criteria using strategies
|
||||
like contains, regex, or exact match.
|
||||
@@ -302,7 +302,7 @@ export default function EvalsTab() {
|
||||
<div className="w-10 h-10 rounded-full bg-sky-500/20 flex items-center justify-center mb-3">
|
||||
<span className="text-lg font-bold text-sky-400">2</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">run</")}</h4>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t("run")}</h4>
|
||||
<p className="text-xs text-text-muted">
|
||||
Execute test cases against your LLM endpoints through OmniRoute. Each case is sent
|
||||
as a real API request.
|
||||
@@ -312,7 +312,7 @@ export default function EvalsTab() {
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 flex items-center justify-center mb-3">
|
||||
<span className="text-lg font-bold text-emerald-400">3</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">evaluate</")}</h4>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t("evaluate")}</h4>
|
||||
<p className="text-xs text-text-muted">
|
||||
Responses are compared against expected criteria. See pass/fail for each case with
|
||||
latency metrics and detailed feedback.
|
||||
|
||||
@@ -376,10 +376,10 @@ export default function ProviderLimits() {
|
||||
className="items-center px-4 py-2.5 border-b border-white/[0.06] text-[11px] font-semibold uppercase tracking-wider text-text-muted"
|
||||
style={{ display: "grid", gridTemplateColumns: "280px 1fr 100px 48px" }}
|
||||
>
|
||||
<div>{t(">account</")}</div>
|
||||
<div>{t("account")}</div>
|
||||
<div>{t("modelQuotas")}</div>
|
||||
<div className="text-center">{t(">lastUsed</")}</div>
|
||||
<div className="text-center">{t(">actions</")}</div>
|
||||
<div className="text-center">{t("lastUsed")}</div>
|
||||
<div className="text-center">{t("actions")}</div>
|
||||
</div>
|
||||
|
||||
{visibleConnections.map((conn, idx) => {
|
||||
|
||||
@@ -68,16 +68,16 @@ export default function SessionsTab() {
|
||||
<thead>
|
||||
<tr className="border-b border-border/30">
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">session</")}
|
||||
{t("session")}
|
||||
</th>
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">age</")}
|
||||
{t("age")}
|
||||
</th>
|
||||
<th className="text-right py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">requests</")}
|
||||
{t("requests")}
|
||||
</th>
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">connection</")}
|
||||
{t("connection")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function ForgotPasswordPage() {
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg p-4">
|
||||
<div className="w-full max-w-lg">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-primary mb-2">{t(">resetPassword</")}</h1>
|
||||
<h1 className="text-3xl font-bold text-primary mb-2">{t("resetPassword")}</h1>
|
||||
<p className="text-text-muted">{t("resetDescription")}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function LoginPage() {
|
||||
rocket_launch
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-text-main tracking-tight">{t(">welcome</")}</h1>
|
||||
<h1 className="text-3xl font-bold text-text-main tracking-tight">{t("welcome")}</h1>
|
||||
<p className="text-text-muted mt-2">
|
||||
Let's get your OmniRoute instance configured
|
||||
</p>
|
||||
@@ -189,13 +189,13 @@ export default function LoginPage() {
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-text-main tracking-tight">OmniRoute</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-text-main tracking-tight">{t(">signIn</")}</h1>
|
||||
<h1 className="text-2xl font-bold text-text-main tracking-tight">{t("signIn")}</h1>
|
||||
<p className="text-text-muted mt-1.5">{t("enterPassword")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-main">{t(">password</")}</label>
|
||||
<label className="text-sm font-medium text-text-main">{t("password")}</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
|
||||
@@ -551,6 +551,7 @@
|
||||
"displayNameHint": "Optional. A friendly name to identify this configuration.",
|
||||
"active": "Active",
|
||||
"activeDescription": "Enable this provider for use in your applications",
|
||||
"cancel": "Cancel",
|
||||
"createProvider": "Create Provider",
|
||||
"failedCreate": "Failed to create provider",
|
||||
"errorOccurred": "An error occurred. Please try again.",
|
||||
@@ -934,6 +935,9 @@
|
||||
"noTranslations": "No translations yet",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"time": "Time",
|
||||
"model": "Model",
|
||||
"status": "Status",
|
||||
"latency": "Latency",
|
||||
"formatConverter": "Format Converter",
|
||||
"input": "Input",
|
||||
@@ -975,7 +979,10 @@
|
||||
"connection": "Connection",
|
||||
"providerLimits": "Provider Limits",
|
||||
"noProviders": "No Providers Connected",
|
||||
"account": "Account",
|
||||
"modelQuotas": "Model Quotas",
|
||||
"lastUsed": "Last Used",
|
||||
"actions": "Actions",
|
||||
"noQuotaData": "No quota data",
|
||||
"noQuotaDataAvailable": "No quota data available"
|
||||
},
|
||||
@@ -1035,6 +1042,7 @@
|
||||
"tokenHealth": "Token Health",
|
||||
"totalOAuth": "Total OAuth",
|
||||
"healthy": "Healthy",
|
||||
"warning": "Warning",
|
||||
"errored": "Errored",
|
||||
"lastCheck": "Last check",
|
||||
"noData": "No data",
|
||||
|
||||
@@ -551,6 +551,7 @@
|
||||
"displayNameHint": "Opcional. Um nome amigável para identificar esta configuração.",
|
||||
"active": "Ativo",
|
||||
"activeDescription": "Habilitar este provedor para uso em suas aplicações",
|
||||
"cancel": "Cancelar",
|
||||
"createProvider": "Criar Provedor",
|
||||
"failedCreate": "Falha ao criar provedor",
|
||||
"errorOccurred": "Ocorreu um erro. Tente novamente.",
|
||||
@@ -934,6 +935,9 @@
|
||||
"noTranslations": "Nenhuma tradução ainda",
|
||||
"source": "Origem",
|
||||
"target": "Destino",
|
||||
"time": "Hora",
|
||||
"model": "Modelo",
|
||||
"status": "Status",
|
||||
"latency": "Latência",
|
||||
"formatConverter": "Conversor de Formato",
|
||||
"input": "Entrada",
|
||||
@@ -975,7 +979,10 @@
|
||||
"connection": "Conexão",
|
||||
"providerLimits": "Limites do Provedor",
|
||||
"noProviders": "Nenhum Provedor Conectado",
|
||||
"account": "Conta",
|
||||
"modelQuotas": "Cotas de Modelo",
|
||||
"lastUsed": "Último uso",
|
||||
"actions": "Ações",
|
||||
"noQuotaData": "Sem dados de cota",
|
||||
"noQuotaDataAvailable": "Nenhum dado de cota disponível"
|
||||
},
|
||||
@@ -1035,6 +1042,7 @@
|
||||
"tokenHealth": "Saúde dos Tokens",
|
||||
"totalOAuth": "Total OAuth",
|
||||
"healthy": "Saudável",
|
||||
"warning": "Aviso",
|
||||
"errored": "Com Erro",
|
||||
"lastCheck": "Última verificação",
|
||||
"noData": "Sem dados",
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function Footer() {
|
||||
|
||||
{/* Product */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t(">product</")}</h4>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t("product")}</h4>
|
||||
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
|
||||
{footerLinks.product.map((link) => (
|
||||
<li key={link.label}>{renderFooterLink(link)}</li>
|
||||
@@ -119,7 +119,7 @@ export default function Footer() {
|
||||
|
||||
{/* Resources */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t(">resources</")}</h4>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t("resources")}</h4>
|
||||
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
|
||||
{footerLinks.resources.map((link) => (
|
||||
<li key={link.label}>{renderFooterLink(link)}</li>
|
||||
@@ -129,7 +129,7 @@ export default function Footer() {
|
||||
|
||||
{/* Company */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t(">company</")}</h4>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t("company")}</h4>
|
||||
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
|
||||
{footerLinks.company.map((link) => (
|
||||
<li key={link.label}>{renderFooterLink(link)}</li>
|
||||
|
||||
@@ -81,18 +81,18 @@ export default function TokenHealthBadge() {
|
||||
<span className="text-text-main">{health.total}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-emerald-400">{t(">healthy</")}</span>
|
||||
<span className="text-emerald-400">{t("healthy")}</span>
|
||||
<span className="text-text-main">{health.healthy}</span>
|
||||
</div>
|
||||
{health.errored > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-red-400">{t(">errored</")}</span>
|
||||
<span className="text-red-400">{t("errored")}</span>
|
||||
<span className="text-text-main">{health.errored}</span>
|
||||
</div>
|
||||
)}
|
||||
{health.warning > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-400">{t(">warning</")}</span>
|
||||
<span className="text-amber-400">{t("warning")}</span>
|
||||
<span className="text-text-main">{health.warning}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -73,6 +73,12 @@ export const updateSettingsSchema = z.object({
|
||||
requireAuthForModels: z.boolean().optional(),
|
||||
blockedProviders: z.array(z.string().max(100)).optional(),
|
||||
hideHealthCheckLogs: z.boolean().optional(),
|
||||
// Routing settings (#134)
|
||||
fallbackStrategy: z
|
||||
.enum(["fill-first", "round-robin", "p2c", "random", "least-used", "cost-optimized"])
|
||||
.optional(),
|
||||
wildcardAliases: z.array(z.object({ pattern: z.string(), target: z.string() })).optional(),
|
||||
stickyRoundRobinLimit: z.number().int().min(0).max(1000).optional(),
|
||||
});
|
||||
|
||||
// ──── Auth Schemas ────
|
||||
|
||||
Reference in New Issue
Block a user