mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Merge branch 'review-pr-983' into release/v3.5.1
This commit is contained in:
1
.playwright-mcp/console-2026-04-04T08-03-37-016Z.log
Normal file
1
.playwright-mcp/console-2026-04-04T08-03-37-016Z.log
Normal file
@@ -0,0 +1 @@
|
||||
[ 32698ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:20130/dashboard/usage?_rsc=18t7j:0
|
||||
158
.playwright-mcp/page-2026-04-04T08-03-38-145Z.yml
Normal file
158
.playwright-mcp/page-2026-04-04T08-03-38-145Z.yml
Normal file
@@ -0,0 +1,158 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- link "Skip to content" [ref=e2] [cursor=pointer]:
|
||||
- /url: "#main-content"
|
||||
- generic [ref=e3]:
|
||||
- complementary [ref=e5]:
|
||||
- link "Skip to content" [ref=e6] [cursor=pointer]:
|
||||
- /url: "#main-content"
|
||||
- link "OmniRoute v3.5.0" [ref=e12] [cursor=pointer]:
|
||||
- /url: /dashboard
|
||||
- img [ref=e14]
|
||||
- generic [ref=e26]:
|
||||
- heading "OmniRoute" [level=1] [ref=e27]
|
||||
- generic [ref=e28]: v3.5.0
|
||||
- navigation "Main navigation" [ref=e29]:
|
||||
- generic [ref=e30]:
|
||||
- link "home Home" [ref=e31] [cursor=pointer]:
|
||||
- /url: /dashboard
|
||||
- generic [ref=e32]: home
|
||||
- generic [ref=e33]: Home
|
||||
- link "api Endpoints" [ref=e34] [cursor=pointer]:
|
||||
- /url: /dashboard/endpoint
|
||||
- generic [ref=e35]: api
|
||||
- generic [ref=e36]: Endpoints
|
||||
- link "vpn_key API Manager" [ref=e37] [cursor=pointer]:
|
||||
- /url: /dashboard/api-manager
|
||||
- generic [ref=e38]: vpn_key
|
||||
- generic [ref=e39]: API Manager
|
||||
- link "dns Providers" [ref=e40] [cursor=pointer]:
|
||||
- /url: /dashboard/providers
|
||||
- generic [ref=e41]: dns
|
||||
- generic [ref=e42]: Providers
|
||||
- link "layers Combos" [ref=e43] [cursor=pointer]:
|
||||
- /url: /dashboard/combos
|
||||
- generic [ref=e44]: layers
|
||||
- generic [ref=e45]: Combos
|
||||
- link "auto_awesome Auto Combo" [ref=e46] [cursor=pointer]:
|
||||
- /url: /dashboard/auto-combo
|
||||
- generic [ref=e47]: auto_awesome
|
||||
- generic [ref=e48]: Auto Combo
|
||||
- link "account_balance_wallet Costs" [ref=e49] [cursor=pointer]:
|
||||
- /url: /dashboard/costs
|
||||
- generic [ref=e50]: account_balance_wallet
|
||||
- generic [ref=e51]: Costs
|
||||
- link "analytics Analytics" [ref=e52] [cursor=pointer]:
|
||||
- /url: /dashboard/analytics
|
||||
- generic [ref=e53]: analytics
|
||||
- generic [ref=e54]: Analytics
|
||||
- link "tune Limits & Quotas" [ref=e55] [cursor=pointer]:
|
||||
- /url: /dashboard/limits
|
||||
- generic [ref=e56]: tune
|
||||
- generic [ref=e57]: Limits & Quotas
|
||||
- link "cached Cache" [ref=e58] [cursor=pointer]:
|
||||
- /url: /dashboard/cache
|
||||
- generic [ref=e59]: cached
|
||||
- generic [ref=e60]: Cache
|
||||
- link "perm_media Media" [ref=e61] [cursor=pointer]:
|
||||
- /url: /dashboard/cache/media
|
||||
- generic [ref=e62]: perm_media
|
||||
- generic [ref=e63]: Media
|
||||
- generic [ref=e64]:
|
||||
- paragraph [ref=e65]: CLI
|
||||
- link "terminal Tools" [ref=e66] [cursor=pointer]:
|
||||
- /url: /dashboard/cli-tools
|
||||
- generic [ref=e67]: terminal
|
||||
- generic [ref=e68]: Tools
|
||||
- link "smart_toy Agents" [ref=e69] [cursor=pointer]:
|
||||
- /url: /dashboard/agents
|
||||
- generic [ref=e70]: smart_toy
|
||||
- generic [ref=e71]: Agents
|
||||
- link "psychology Memory" [ref=e72] [cursor=pointer]:
|
||||
- /url: /dashboard/memory
|
||||
- generic [ref=e73]: psychology
|
||||
- generic [ref=e74]: Memory
|
||||
- link "auto_fix_high Skills" [ref=e75] [cursor=pointer]:
|
||||
- /url: /dashboard/skills
|
||||
- generic [ref=e76]: auto_fix_high
|
||||
- generic [ref=e77]: Skills
|
||||
- generic [ref=e78]:
|
||||
- paragraph [ref=e79]: System
|
||||
- link "health_and_safety Health" [ref=e80] [cursor=pointer]:
|
||||
- /url: /dashboard/health
|
||||
- generic [ref=e81]: health_and_safety
|
||||
- generic [ref=e82]: Health
|
||||
- link "description Logs" [ref=e83] [cursor=pointer]:
|
||||
- /url: /dashboard/logs
|
||||
- generic [ref=e84]: description
|
||||
- generic [ref=e85]: Logs
|
||||
- link "history Audit Log" [ref=e86] [cursor=pointer]:
|
||||
- /url: /dashboard/audit
|
||||
- generic [ref=e87]: history
|
||||
- generic [ref=e88]: Audit Log
|
||||
- link "settings Settings" [ref=e89] [cursor=pointer]:
|
||||
- /url: /dashboard/settings
|
||||
- generic [ref=e90]: settings
|
||||
- generic [ref=e91]: Settings
|
||||
- generic [ref=e92]:
|
||||
- paragraph [ref=e93]: Help
|
||||
- link "menu_book Docs" [ref=e94] [cursor=pointer]:
|
||||
- /url: /docs
|
||||
- generic [ref=e95]: menu_book
|
||||
- generic [ref=e96]: Docs
|
||||
- link "bug_report Issues" [ref=e97] [cursor=pointer]:
|
||||
- /url: https://github.com/diegosouzapw/OmniRoute/issues
|
||||
- generic [ref=e98]: bug_report
|
||||
- generic [ref=e99]: Issues
|
||||
- generic [ref=e100]:
|
||||
- button "restart_alt Restart" [ref=e101]:
|
||||
- generic: restart_alt
|
||||
- text: Restart
|
||||
- button "power_settings_new Shutdown" [ref=e102]:
|
||||
- generic: power_settings_new
|
||||
- text: Shutdown
|
||||
- main [ref=e103]:
|
||||
- generic [ref=e104]:
|
||||
- button "menu" [ref=e106]:
|
||||
- generic: menu
|
||||
- generic [ref=e107]:
|
||||
- button "🇺🇸 EN expand_more" [ref=e109]:
|
||||
- generic [ref=e110]: 🇺🇸
|
||||
- generic [ref=e111]: EN
|
||||
- generic: expand_more
|
||||
- button "Switch to dark mode" [ref=e112]:
|
||||
- generic: dark_mode
|
||||
- button "logout" [ref=e113]:
|
||||
- generic: logout
|
||||
- generic [ref=e115]:
|
||||
- navigation "Breadcrumb" [ref=e116]:
|
||||
- link "Dashboard" [ref=e118] [cursor=pointer]:
|
||||
- /url: /dashboard
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e120]: ›
|
||||
- generic [ref=e121]: Onboarding
|
||||
- generic [ref=e123]:
|
||||
- generic [ref=e124]:
|
||||
- generic [ref=e126]: "1"
|
||||
- generic [ref=e129]: "2"
|
||||
- generic [ref=e132]: "3"
|
||||
- generic [ref=e135]: "4"
|
||||
- generic [ref=e138]: "5"
|
||||
- generic [ref=e139]:
|
||||
- generic [ref=e140]:
|
||||
- generic [ref=e141]: waving_hand
|
||||
- heading "Welcome" [level=2] [ref=e142]
|
||||
- generic [ref=e144]:
|
||||
- paragraph [ref=e145]: OmniRoute is your local AI API proxy. It routes requests to multiple AI providers with load balancing, failover, and usage tracking.
|
||||
- generic [ref=e146]:
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]: swap_horiz
|
||||
- text: Multi-Provider
|
||||
- generic [ref=e149]:
|
||||
- generic [ref=e150]: monitoring
|
||||
- text: Usage Tracking
|
||||
- generic [ref=e151]:
|
||||
- generic [ref=e152]: shield
|
||||
- text: API Key Mgmt
|
||||
- button "Get Started" [ref=e155] [cursor=pointer]
|
||||
- button "Skip wizard entirely" [ref=e157] [cursor=pointer]
|
||||
- alert [ref=e158]
|
||||
1011
.playwright-mcp/page-2026-04-04T08-04-15-839Z.yml
Normal file
1011
.playwright-mcp/page-2026-04-04T08-04-15-839Z.yml
Normal file
File diff suppressed because it is too large
Load Diff
132
.playwright-mcp/page-2026-04-04T08-04-46-313Z.yml
Normal file
132
.playwright-mcp/page-2026-04-04T08-04-46-313Z.yml
Normal file
@@ -0,0 +1,132 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- link "Skip to content" [ref=e2] [cursor=pointer]:
|
||||
- /url: "#main-content"
|
||||
- generic [ref=e3]:
|
||||
- complementary [ref=e5]:
|
||||
- link "Skip to content" [ref=e6] [cursor=pointer]:
|
||||
- /url: "#main-content"
|
||||
- link "OmniRoute v3.5.0" [ref=e12] [cursor=pointer]:
|
||||
- /url: /dashboard
|
||||
- img [ref=e14]
|
||||
- generic [ref=e26]:
|
||||
- heading "OmniRoute" [level=1] [ref=e27]
|
||||
- generic [ref=e28]: v3.5.0
|
||||
- navigation "Main navigation" [ref=e29]:
|
||||
- generic [ref=e30]:
|
||||
- link "home Home" [ref=e31] [cursor=pointer]:
|
||||
- /url: /dashboard
|
||||
- generic [ref=e32]: home
|
||||
- generic [ref=e33]: Home
|
||||
- link "api Endpoints" [ref=e34] [cursor=pointer]:
|
||||
- /url: /dashboard/endpoint
|
||||
- generic [ref=e35]: api
|
||||
- generic [ref=e36]: Endpoints
|
||||
- link "vpn_key API Manager" [ref=e37] [cursor=pointer]:
|
||||
- /url: /dashboard/api-manager
|
||||
- generic [ref=e38]: vpn_key
|
||||
- generic [ref=e39]: API Manager
|
||||
- link "dns Providers" [ref=e40] [cursor=pointer]:
|
||||
- /url: /dashboard/providers
|
||||
- generic [ref=e41]: dns
|
||||
- generic [ref=e42]: Providers
|
||||
- link "layers Combos" [ref=e43] [cursor=pointer]:
|
||||
- /url: /dashboard/combos
|
||||
- generic [ref=e44]: layers
|
||||
- generic [ref=e45]: Combos
|
||||
- link "auto_awesome Auto Combo" [ref=e46] [cursor=pointer]:
|
||||
- /url: /dashboard/auto-combo
|
||||
- generic [ref=e47]: auto_awesome
|
||||
- generic [ref=e48]: Auto Combo
|
||||
- link "account_balance_wallet Costs" [ref=e49] [cursor=pointer]:
|
||||
- /url: /dashboard/costs
|
||||
- generic [ref=e50]: account_balance_wallet
|
||||
- generic [ref=e51]: Costs
|
||||
- link "analytics Analytics" [ref=e52] [cursor=pointer]:
|
||||
- /url: /dashboard/analytics
|
||||
- generic [ref=e53]: analytics
|
||||
- generic [ref=e54]: Analytics
|
||||
- link "tune Limits & Quotas" [ref=e55] [cursor=pointer]:
|
||||
- /url: /dashboard/limits
|
||||
- generic [ref=e56]: tune
|
||||
- generic [ref=e57]: Limits & Quotas
|
||||
- link "cached Cache" [ref=e58] [cursor=pointer]:
|
||||
- /url: /dashboard/cache
|
||||
- generic [ref=e59]: cached
|
||||
- generic [ref=e60]: Cache
|
||||
- link "perm_media Media" [ref=e61] [cursor=pointer]:
|
||||
- /url: /dashboard/cache/media
|
||||
- generic [ref=e62]: perm_media
|
||||
- generic [ref=e63]: Media
|
||||
- generic [ref=e64]:
|
||||
- paragraph [ref=e65]: CLI
|
||||
- link "terminal Tools" [ref=e66] [cursor=pointer]:
|
||||
- /url: /dashboard/cli-tools
|
||||
- generic [ref=e67]: terminal
|
||||
- generic [ref=e68]: Tools
|
||||
- link "smart_toy Agents" [ref=e69] [cursor=pointer]:
|
||||
- /url: /dashboard/agents
|
||||
- generic [ref=e70]: smart_toy
|
||||
- generic [ref=e71]: Agents
|
||||
- link "psychology Memory" [ref=e72] [cursor=pointer]:
|
||||
- /url: /dashboard/memory
|
||||
- generic [ref=e73]: psychology
|
||||
- generic [ref=e74]: Memory
|
||||
- link "auto_fix_high Skills" [ref=e75] [cursor=pointer]:
|
||||
- /url: /dashboard/skills
|
||||
- generic [ref=e76]: auto_fix_high
|
||||
- generic [ref=e77]: Skills
|
||||
- generic [ref=e78]:
|
||||
- paragraph [ref=e79]: System
|
||||
- link "health_and_safety Health" [ref=e80] [cursor=pointer]:
|
||||
- /url: /dashboard/health
|
||||
- generic [ref=e81]: health_and_safety
|
||||
- generic [ref=e82]: Health
|
||||
- link "description Logs" [ref=e83] [cursor=pointer]:
|
||||
- /url: /dashboard/logs
|
||||
- generic [ref=e84]: description
|
||||
- generic [ref=e85]: Logs
|
||||
- link "history Audit Log" [ref=e86] [cursor=pointer]:
|
||||
- /url: /dashboard/audit
|
||||
- generic [ref=e87]: history
|
||||
- generic [ref=e88]: Audit Log
|
||||
- link "settings Settings" [ref=e89] [cursor=pointer]:
|
||||
- /url: /dashboard/settings
|
||||
- generic [ref=e90]: settings
|
||||
- generic [ref=e91]: Settings
|
||||
- generic [ref=e92]:
|
||||
- paragraph [ref=e93]: Help
|
||||
- link "menu_book Docs" [ref=e94] [cursor=pointer]:
|
||||
- /url: /docs
|
||||
- generic [ref=e95]: menu_book
|
||||
- generic [ref=e96]: Docs
|
||||
- link "bug_report Issues" [ref=e97] [cursor=pointer]:
|
||||
- /url: https://github.com/diegosouzapw/OmniRoute/issues
|
||||
- generic [ref=e98]: bug_report
|
||||
- generic [ref=e99]: Issues
|
||||
- generic [ref=e100]:
|
||||
- button "restart_alt Restart" [ref=e101]:
|
||||
- generic: restart_alt
|
||||
- text: Restart
|
||||
- button "power_settings_new Shutdown" [ref=e102]:
|
||||
- generic: power_settings_new
|
||||
- text: Shutdown
|
||||
- main [ref=e103]:
|
||||
- generic [ref=e104]:
|
||||
- button "menu" [ref=e106]:
|
||||
- generic: menu
|
||||
- generic [ref=e107]:
|
||||
- button "🇺🇸 EN expand_more" [ref=e109]:
|
||||
- generic [ref=e110]: 🇺🇸
|
||||
- generic [ref=e111]: EN
|
||||
- generic: expand_more
|
||||
- button "Switch to dark mode" [ref=e112]:
|
||||
- generic: dark_mode
|
||||
- button "logout" [ref=e113]:
|
||||
- generic: logout
|
||||
- navigation "Breadcrumb" [ref=e116]:
|
||||
- link "Dashboard" [ref=e118] [cursor=pointer]:
|
||||
- /url: /dashboard
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e120]: ›
|
||||
- generic [ref=e121]: Providers
|
||||
- alert [ref=e135]
|
||||
@@ -0,0 +1,346 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ModelsDevStatus {
|
||||
enabled: boolean;
|
||||
lastSync: string | null;
|
||||
lastSyncModelCount: number;
|
||||
lastSyncCapabilityCount: number;
|
||||
nextSync: string | null;
|
||||
intervalMs: number;
|
||||
providerCount: number;
|
||||
modelCount: number;
|
||||
capabilityCount: number;
|
||||
}
|
||||
|
||||
interface SyncResult {
|
||||
success: boolean;
|
||||
modelCount: number;
|
||||
providerCount: number;
|
||||
capabilityCount: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export default function ModelsDevSyncTab() {
|
||||
const t = useTranslations("settings");
|
||||
const [status, setStatus] = useState<ModelsDevStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [intervalHours, setIntervalHours] = useState(24);
|
||||
const [feedback, setFeedback] = useState<{ type: "success" | "error"; message: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/settings/models-dev?action=status");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setStatus(data);
|
||||
}
|
||||
} catch {
|
||||
// Silently fail — sync may not be initialized yet
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([fetchStatus(), fetch("/api/settings").then((r) => (r.ok ? r.json() : null))])
|
||||
.then(([, settingsData]) => {
|
||||
if (settingsData) {
|
||||
setEnabled(settingsData.modelsDevSyncEnabled === true);
|
||||
const intervalMs = settingsData.modelsDevSyncInterval || 86400000;
|
||||
setIntervalHours(Math.round(intervalMs / 3600000));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch models.dev settings:", err);
|
||||
setFeedback({ type: "error", message: "Failed to load settings" });
|
||||
});
|
||||
}, [fetchStatus]);
|
||||
|
||||
const triggerSync = async () => {
|
||||
setSyncing(true);
|
||||
setFeedback(null);
|
||||
try {
|
||||
const res = await fetch("/api/settings/models-dev", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "sync" }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const result: SyncResult = await res.json();
|
||||
if (result.success) {
|
||||
setFeedback({
|
||||
type: "success",
|
||||
message: `Synced ${result.modelCount.toLocaleString()} pricing entries, ${result.capabilityCount.toLocaleString()} capabilities`,
|
||||
});
|
||||
fetchStatus();
|
||||
} else {
|
||||
setFeedback({ type: "error", message: result.error || "Sync failed" });
|
||||
}
|
||||
} else {
|
||||
setFeedback({ type: "error", message: "Sync request failed" });
|
||||
}
|
||||
} catch {
|
||||
setFeedback({ type: "error", message: "Network error" });
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
setTimeout(() => setFeedback(null), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEnabled = async () => {
|
||||
const newVal = !enabled;
|
||||
setEnabled(newVal);
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ modelsDevSyncEnabled: newVal }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setEnabled(!newVal);
|
||||
setFeedback({ type: "error", message: t("enableSyncError") || "Failed to update" });
|
||||
} else {
|
||||
setFeedback({ type: "success", message: "Settings saved" });
|
||||
}
|
||||
} catch {
|
||||
setEnabled(!newVal);
|
||||
setFeedback({ type: "error", message: "Network error" });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
setTimeout(() => setFeedback(null), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const updateInterval = async (hours: number) => {
|
||||
const oldInterval = intervalHours;
|
||||
setIntervalHours(hours);
|
||||
try {
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ modelsDevSyncInterval: hours * 3600000 }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setIntervalHours(oldInterval);
|
||||
setFeedback({ type: "error", message: t("enableSyncError") || "Failed to update" });
|
||||
} else {
|
||||
setFeedback({ type: "success", message: "Interval updated" });
|
||||
}
|
||||
} catch {
|
||||
setIntervalHours(oldInterval);
|
||||
setFeedback({ type: "error", message: "Network error" });
|
||||
} finally {
|
||||
setTimeout(() => setFeedback(null), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
sync
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{t("modelsDevTitle")}</h3>
|
||||
<p className="text-sm text-text-muted">{t("modelsDevDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-text-muted">{t("loading")}...</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const formatLastSync = (iso: string | null) => {
|
||||
if (!iso) return t("never");
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
if (diffMin < 1) return t("justNow");
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
if (diffHr < 24) return `${diffHr}h ago`;
|
||||
return d.toLocaleDateString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Main sync card */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
database
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{t("modelsDevTitle")}</h3>
|
||||
<p className="text-sm text-text-muted">{t("modelsDevDesc")}</p>
|
||||
</div>
|
||||
{feedback && (
|
||||
<span
|
||||
className={`ml-auto text-xs font-medium flex items-center gap-1 ${
|
||||
feedback.type === "success" ? "text-emerald-500" : "text-red-500"
|
||||
}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{feedback.type === "success" ? "check_circle" : "error"}
|
||||
</span>{" "}
|
||||
{feedback.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Enable toggle */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-surface/30 border border-border/30 mb-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{t("modelsDevEnabled")}</p>
|
||||
<p className="text-xs text-text-muted mt-0.5">{t("modelsDevEnabledDesc")}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleEnabled}
|
||||
disabled={saving}
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${
|
||||
enabled ? "bg-blue-500" : "bg-border"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform ${
|
||||
enabled ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sync interval */}
|
||||
{enabled && (
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30 mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm font-medium">{t("modelsDevInterval")}</p>
|
||||
<span className="text-sm font-mono tabular-nums text-blue-400">{intervalHours}h</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="168"
|
||||
step="1"
|
||||
value={intervalHours}
|
||||
onChange={(e) => updateInterval(parseInt(e.target.value))}
|
||||
className="w-full accent-blue-500"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-text-muted mt-1">
|
||||
<span>1h</span>
|
||||
<span>6h</span>
|
||||
<span>24h</span>
|
||||
<span>7d</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manual sync button */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={triggerSync}
|
||||
disabled={syncing}
|
||||
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${syncing ? "animate-spin" : ""}`}
|
||||
>
|
||||
sync
|
||||
</span>
|
||||
{syncing ? t("syncing") : t("syncNow")}
|
||||
</button>
|
||||
{status?.lastSync && (
|
||||
<span className="text-xs text-text-muted">
|
||||
{t("lastSync")}: {formatLastSync(status.lastSync)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Stats card */}
|
||||
{status && (status.providerCount > 0 || status.modelCount > 0) && (
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-500">
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
bar_chart
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{t("modelsDevStats")}</h3>
|
||||
<p className="text-sm text-text-muted">{t("modelsDevStatsDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30 text-center">
|
||||
<p className="text-2xl font-bold text-blue-400">
|
||||
{status.providerCount.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1">{t("providers")}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30 text-center">
|
||||
<p className="text-2xl font-bold text-emerald-400">
|
||||
{status.modelCount.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1">{t("modelsWithPricing")}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30 text-center">
|
||||
<p className="text-2xl font-bold text-violet-400">
|
||||
{status.capabilityCount.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1">{t("capabilities")}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30 text-center">
|
||||
<p className="text-2xl font-bold text-amber-400">
|
||||
{status.lastSyncModelCount > 0 ? status.lastSyncModelCount.toLocaleString() : "—"}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1">{t("lastSyncCount")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status.lastSync && (
|
||||
<div className="mt-4 text-xs text-text-muted text-center">
|
||||
{t("lastSyncFull")}: {new Date(status.lastSync).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Info card */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-500">
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
info
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{t("modelsDevInfo")}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-text-muted space-y-2">
|
||||
<p>{t("modelsDevInfoDesc")}</p>
|
||||
<p>{t("modelsDevInfoResolution")}</p>
|
||||
<p className="text-xs font-mono bg-surface/50 p-2 rounded">{t("modelsDevInfoOrder")}</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import ModelAliasesTab from "./components/ModelAliasesTab";
|
||||
import BackgroundDegradationTab from "./components/BackgroundDegradationTab";
|
||||
import CacheSettingsTab from "./components/CacheSettingsTab";
|
||||
import MemorySkillsTab from "./components/MemorySkillsTab";
|
||||
import ModelsDevSyncTab from "./components/ModelsDevSyncTab";
|
||||
import ResilienceTab from "./components/ResilienceTab";
|
||||
import CliproxyapiSettingsTab from "./components/CliproxyapiSettingsTab";
|
||||
|
||||
@@ -95,6 +96,7 @@ export default function SettingsPage() {
|
||||
<SystemPromptTab />
|
||||
<CacheSettingsTab />
|
||||
<MemorySkillsTab />
|
||||
<ModelsDevSyncTab />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
92
src/app/api/settings/models-dev/route.ts
Normal file
92
src/app/api/settings/models-dev/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import {
|
||||
syncModelsDev,
|
||||
getModelsDevPricing,
|
||||
getSyncedCapabilities,
|
||||
getSyncStatus,
|
||||
startPeriodicSync,
|
||||
stopPeriodicSync,
|
||||
} from "@/lib/modelsDevSync";
|
||||
|
||||
const modelsDevActionSchema = z.object({
|
||||
action: z.enum(["sync", "start", "stop"]),
|
||||
dryRun: z.boolean().optional(),
|
||||
syncCapabilities: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const action = searchParams.get("action");
|
||||
|
||||
if (action === "status") {
|
||||
const status = getSyncStatus();
|
||||
const pricing = getModelsDevPricing();
|
||||
const caps = getSyncedCapabilities();
|
||||
|
||||
const providerCount = Object.keys(pricing).length;
|
||||
const modelCount = Object.values(pricing).reduce(
|
||||
(sum, models) => sum + Object.keys(models).length,
|
||||
0
|
||||
);
|
||||
const capabilityCount = Object.values(caps).reduce(
|
||||
(sum, models) => sum + Object.keys(models).length,
|
||||
0
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
...status,
|
||||
providerCount,
|
||||
modelCount,
|
||||
capabilityCount,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const validation = validateBody(modelsDevActionSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return validation.response;
|
||||
}
|
||||
|
||||
const { action, dryRun, syncCapabilities } = validation.data;
|
||||
|
||||
if (action === "sync") {
|
||||
const result = await syncModelsDev({
|
||||
dryRun: dryRun ?? false,
|
||||
syncCapabilities: syncCapabilities !== false,
|
||||
});
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
if (action === "start") {
|
||||
startPeriodicSync();
|
||||
return NextResponse.json({ success: true, message: "Periodic sync started" });
|
||||
}
|
||||
|
||||
if (action === "stop") {
|
||||
stopPeriodicSync();
|
||||
return NextResponse.json({ success: true, message: "Periodic sync stopped" });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
||||
}
|
||||
@@ -44,6 +44,12 @@ export async function PATCH(request) {
|
||||
try {
|
||||
const rawBody = await request.json();
|
||||
|
||||
// Capture old settings BEFORE update for comparison (needed for models.dev sync toggle)
|
||||
const oldSettings =
|
||||
"modelsDevSyncEnabled" in rawBody || "modelsDevSyncInterval" in rawBody
|
||||
? await getSettings()
|
||||
: null;
|
||||
|
||||
// Zod validation
|
||||
const validation = validateBody(updateSettingsSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
@@ -149,6 +155,26 @@ export async function PATCH(request) {
|
||||
invalidateCacheControlSettingsCache();
|
||||
}
|
||||
|
||||
// Sync models.dev sync settings (compare old vs new state)
|
||||
if (oldSettings && ("modelsDevSyncEnabled" in body || "modelsDevSyncInterval" in body)) {
|
||||
const { stopPeriodicSync, startPeriodicSync } = await import("@/lib/modelsDevSync");
|
||||
const wasEnabled = (oldSettings as Record<string, unknown>).modelsDevSyncEnabled === true;
|
||||
const isEnabled = settings.modelsDevSyncEnabled === true;
|
||||
const oldInterval = (oldSettings as Record<string, unknown>).modelsDevSyncInterval as
|
||||
| number
|
||||
| undefined;
|
||||
const newInterval = settings.modelsDevSyncInterval as number | undefined;
|
||||
|
||||
if (wasEnabled && !isEnabled) {
|
||||
stopPeriodicSync();
|
||||
} else if (!wasEnabled && isEnabled) {
|
||||
startPeriodicSync(newInterval);
|
||||
} else if (isEnabled && oldInterval !== newInterval) {
|
||||
stopPeriodicSync();
|
||||
startPeriodicSync(newInterval);
|
||||
}
|
||||
}
|
||||
|
||||
const { password, ...safeSettings } = settings;
|
||||
return NextResponse.json(safeSettings);
|
||||
} catch (error) {
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "يمكنك تجاوز التسعير الافتراضي لنماذج محددة. تحظى التجاوزات المخصصة بالأولوية على الأسعار التي يتم اكتشافها تلقائيًا.",
|
||||
"editPricing": "تحرير التسعير",
|
||||
"viewFullDetails": "عرض التفاصيل الكاملة",
|
||||
"themeCoral": "مرجاني"
|
||||
"themeCoral": "مرجاني",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "مترجم",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Можете да замените цените по подразбиране за конкретни модели. Персонализираните замени имат приоритет пред автоматично разпознатото ценообразуване.",
|
||||
"editPricing": "Редактиране на цените",
|
||||
"viewFullDetails": "Вижте пълните подробности",
|
||||
"themeCoral": "Корал"
|
||||
"themeCoral": "Корал",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Преводач",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Výchozí ceny pro konkrétní modely můžete přepsat. Vlastní přepsání má přednost před automaticky zjištěnými cenami.",
|
||||
"editPricing": "Upravit ceny",
|
||||
"viewFullDetails": "Zobrazit všechny podrobnosti",
|
||||
"themeCoral": "Korál"
|
||||
"themeCoral": "Korál",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Překladatel",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Du kan tilsidesætte standardpriser for specifikke modeller. Tilpassede tilsidesættelser har prioritet frem for automatisk registrerede priser.",
|
||||
"editPricing": "Rediger prissætning",
|
||||
"viewFullDetails": "Se alle detaljer",
|
||||
"themeCoral": "Koral"
|
||||
"themeCoral": "Koral",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Oversætter",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Sie können die Standardpreise für bestimmte Modelle überschreiben. Benutzerdefinierte Überschreibungen haben Vorrang vor automatisch erkannten Preisen.",
|
||||
"editPricing": "Preise bearbeiten",
|
||||
"viewFullDetails": "Vollständige Details anzeigen",
|
||||
"themeCoral": "Koralle"
|
||||
"themeCoral": "Koralle",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Übersetzer",
|
||||
|
||||
@@ -1827,6 +1827,27 @@
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"justNow": "Just now",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"providers": "Providers",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"systemTheme": "System Theme",
|
||||
"debugToggle": "Enable Debug Mode",
|
||||
"sidebarVisibilityToggle": "Show Sidebar Items",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Puede anular los precios predeterminados para modelos específicos. Las anulaciones personalizadas tienen prioridad sobre los precios detectados automáticamente.",
|
||||
"editPricing": "Editar precios",
|
||||
"viewFullDetails": "Ver todos los detalles",
|
||||
"themeCoral": "Coral"
|
||||
"themeCoral": "Coral",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Traductor",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Voit ohittaa tiettyjen mallien oletushinnoittelun. Mukautetut ohitukset ovat etusijalla automaattisesti tunnistettuihin hinnoitteluun nähden.",
|
||||
"editPricing": "Muokkaa hinnoittelua",
|
||||
"viewFullDetails": "Näytä täydelliset tiedot",
|
||||
"themeCoral": "Koralli"
|
||||
"themeCoral": "Koralli",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Kääntäjä",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Vous pouvez remplacer le prix par défaut pour des modèles spécifiques. Les remplacements personnalisés ont la priorité sur les prix détectés automatiquement.",
|
||||
"editPricing": "Modifier le prix",
|
||||
"viewFullDetails": "Afficher tous les détails",
|
||||
"themeCoral": "Corail"
|
||||
"themeCoral": "Corail",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Traducteur",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "אתה יכול לעקוף את תמחור ברירת המחדל עבור דגמים ספציפיים. עקיפות מותאמות אישית מקבלות עדיפות על פני תמחור שזוהה אוטומטית.",
|
||||
"editPricing": "ערוך תמחור",
|
||||
"viewFullDetails": "צפה בפרטים המלאים",
|
||||
"themeCoral": "אלמוג"
|
||||
"themeCoral": "אלמוג",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "מתרגם",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "आप विशिष्ट मॉडलों के लिए डिफ़ॉल्ट मूल्य निर्धारण को ओवरराइड कर सकते हैं। कस्टम ओवरराइड्स को स्वतः-पता लगाए गए मूल्य-निर्धारण पर प्राथमिकता दी जाती है।",
|
||||
"editPricing": "मूल्य निर्धारण संपादित करें",
|
||||
"viewFullDetails": "पूर्ण विवरण देखें",
|
||||
"themeCoral": "कोरल"
|
||||
"themeCoral": "कोरल",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "अनुवादक",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Egyes modelleknél felülbírálhatja az alapértelmezett árazást. Az egyéni felülbírálások elsőbbséget élveznek az automatikusan észlelt árképzéssel szemben.",
|
||||
"editPricing": "Árak szerkesztése",
|
||||
"viewFullDetails": "Teljes részletek megtekintése",
|
||||
"themeCoral": "Korall"
|
||||
"themeCoral": "Korall",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Fordító",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Anda dapat mengganti harga default untuk model tertentu. Penggantian khusus lebih diprioritaskan dibandingkan harga yang terdeteksi otomatis.",
|
||||
"editPricing": "Sunting Harga",
|
||||
"viewFullDetails": "Lihat Detail Lengkap",
|
||||
"themeCoral": "Koral"
|
||||
"themeCoral": "Koral",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Penerjemah",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Puoi sostituire i prezzi predefiniti per modelli specifici. Le sostituzioni personalizzate hanno la priorità sui prezzi rilevati automaticamente.",
|
||||
"editPricing": "Modifica prezzi",
|
||||
"viewFullDetails": "Visualizza i dettagli completi",
|
||||
"themeCoral": "Corallo"
|
||||
"themeCoral": "Corallo",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Traduttore",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "特定のモデルのデフォルトの価格をオーバーライドできます。カスタム オーバーライドは、自動検出された価格設定よりも優先されます。",
|
||||
"editPricing": "価格の編集",
|
||||
"viewFullDetails": "詳細を表示",
|
||||
"themeCoral": "コーラル"
|
||||
"themeCoral": "コーラル",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "翻訳者",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "특정 모델의 기본 가격을 재정의할 수 있습니다. 맞춤 재정의는 자동 감지된 가격보다 우선 적용됩니다.",
|
||||
"editPricing": "가격 편집",
|
||||
"viewFullDetails": "전체 세부정보 보기",
|
||||
"themeCoral": "코랄"
|
||||
"themeCoral": "코랄",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "번역기",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Anda boleh mengatasi harga lalai untuk model tertentu. Penggantian tersuai diutamakan berbanding harga yang dikesan secara automatik.",
|
||||
"editPricing": "Edit Harga",
|
||||
"viewFullDetails": "Lihat Butiran Penuh",
|
||||
"themeCoral": "Koral"
|
||||
"themeCoral": "Koral",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Penterjemah",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "U kunt de standaardprijzen voor specifieke modellen overschrijven. Aangepaste overschrijvingen hebben voorrang op automatisch gedetecteerde prijzen.",
|
||||
"editPricing": "Prijzen bewerken",
|
||||
"viewFullDetails": "Bekijk volledige details",
|
||||
"themeCoral": "Koraal"
|
||||
"themeCoral": "Koraal",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Vertaler",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Du kan overstyre standardpriser for spesifikke modeller. Egendefinerte overstyringer prioriteres fremfor automatisk oppdagede priser.",
|
||||
"editPricing": "Rediger priser",
|
||||
"viewFullDetails": "Se alle detaljer",
|
||||
"themeCoral": "Korall"
|
||||
"themeCoral": "Korall",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Oversetter",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Maaari mong i-override ang default na pagpepresyo para sa mga partikular na modelo. Mas inuuna ang mga custom na override kaysa sa awtomatikong natukoy na pagpepresyo.",
|
||||
"editPricing": "I-edit ang Pagpepresyo",
|
||||
"viewFullDetails": "Tingnan ang Buong Detalye",
|
||||
"themeCoral": "Coral"
|
||||
"themeCoral": "Coral",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tagasalin",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Możesz zastąpić domyślne ceny dla określonych modeli. Zastąpienia niestandardowe mają pierwszeństwo przed automatycznie wykrytymi cenami.",
|
||||
"editPricing": "Edytuj ceny",
|
||||
"viewFullDetails": "Zobacz pełne szczegóły",
|
||||
"themeCoral": "Koral"
|
||||
"themeCoral": "Koral",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tłumacz",
|
||||
|
||||
@@ -2250,7 +2250,26 @@
|
||||
"memorySkillsTitle": "Memória e Skills",
|
||||
"memorySkillsDesc": "Contexto persistente e capacidades A2A",
|
||||
"purgeExpiredLogs": "Purgar Logs Expirados",
|
||||
"purgeLogsFailed": "Falha ao purgar logs"
|
||||
"purgeLogsFailed": "Falha ao purgar logs",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tradutor",
|
||||
|
||||
@@ -2250,7 +2250,26 @@
|
||||
"memorySkillsTitle": "Memória e Skills",
|
||||
"memorySkillsDesc": "Contexto persistente e capacidades A2A",
|
||||
"purgeExpiredLogs": "Purgar Logs Expirados",
|
||||
"purgeLogsFailed": "Falha ao purgar logs"
|
||||
"purgeLogsFailed": "Falha ao purgar logs",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tradutor",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Puteți suprascrie prețurile implicite pentru anumite modele. Anulările personalizate au prioritate față de prețurile detectate automat.",
|
||||
"editPricing": "Editați prețul",
|
||||
"viewFullDetails": "Vezi detalii complete",
|
||||
"themeCoral": "Coral"
|
||||
"themeCoral": "Coral",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Traducător",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Вы можете переопределить цены по умолчанию для определенных моделей. Пользовательские переопределения имеют приоритет над ценами, определяемыми автоматически.",
|
||||
"editPricing": "Изменить цену",
|
||||
"viewFullDetails": "Посмотреть полную информацию",
|
||||
"themeCoral": "Коралл"
|
||||
"themeCoral": "Коралл",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Переводчик",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Predvolené ceny pre konkrétne modely môžete prepísať. Vlastné prepísania majú prednosť pred automaticky zistenými cenami.",
|
||||
"editPricing": "Upraviť ceny",
|
||||
"viewFullDetails": "Zobraziť úplné podrobnosti",
|
||||
"themeCoral": "Korál"
|
||||
"themeCoral": "Korál",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Prekladateľ",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Du kan åsidosätta standardpriser för specifika modeller. Anpassade åsidosättningar har prioritet framför automatiskt identifierade priser.",
|
||||
"editPricing": "Redigera prissättning",
|
||||
"viewFullDetails": "Visa fullständiga detaljer",
|
||||
"themeCoral": "Korall"
|
||||
"themeCoral": "Korall",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Översättare",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "คุณสามารถแทนที่ราคาเริ่มต้นสำหรับรุ่นเฉพาะได้ การแทนที่แบบกำหนดเองจะมีลำดับความสำคัญมากกว่าการกำหนดราคาที่ตรวจพบอัตโนมัติ",
|
||||
"editPricing": "แก้ไขราคา",
|
||||
"viewFullDetails": "ดูรายละเอียดทั้งหมด",
|
||||
"themeCoral": "คอรัล"
|
||||
"themeCoral": "คอรัล",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "นักแปล",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Belirli modeller için varsayılan fiyatlandırmayı geçersiz kılabilirsiniz. Özel geçersiz kılmalar, otomatik algılanan fiyatlandırmaya göre öncelik kazanır.",
|
||||
"editPricing": "Fiyatlandırmayı Düzenle",
|
||||
"viewFullDetails": "Tüm Ayrıntıları Görüntüle",
|
||||
"themeCoral": "Mercan"
|
||||
"themeCoral": "Mercan",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Çeviri",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Ви можете змінити ціни за умовчанням для певних моделей. Спеціальні зміни мають пріоритет над автоматично визначеними цінами.",
|
||||
"editPricing": "Редагувати ціни",
|
||||
"viewFullDetails": "Переглянути повну інформацію",
|
||||
"themeCoral": "Корал"
|
||||
"themeCoral": "Корал",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Перекладач",
|
||||
|
||||
@@ -2164,7 +2164,46 @@
|
||||
"customPricingNote": "Bạn có thể ghi đè giá mặc định cho các kiểu máy cụ thể. Ghi đè tùy chỉnh được ưu tiên hơn giá được tự động phát hiện.",
|
||||
"editPricing": "Chỉnh sửa giá",
|
||||
"viewFullDetails": "Xem chi tiết đầy đủ",
|
||||
"themeCoral": "San hô"
|
||||
"themeCoral": "San hô",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Người phiên dịch",
|
||||
|
||||
@@ -2176,7 +2176,46 @@
|
||||
"lkgpCacheClearFailed": "清除 LKGP 缓存失败",
|
||||
"maintenance": "维护",
|
||||
"purgeExpiredLogs": "清理过期日志",
|
||||
"purgeLogsFailed": "清理日志失败"
|
||||
"purgeLogsFailed": "清理日志失败",
|
||||
"modelsDevTitle": "Model Database",
|
||||
"modelsDevDesc": "Auto-sync pricing, capabilities & specs from models.dev",
|
||||
"modelsDevEnabled": "Enable models.dev Sync",
|
||||
"modelsDevEnabledDesc": "Fetch model pricing, capabilities, and specs from the open-source models.dev database",
|
||||
"modelsDevInterval": "Sync Interval",
|
||||
"syncNow": "Sync Now",
|
||||
"syncing": "Syncing...",
|
||||
"lastSync": "Last sync",
|
||||
"never": "Never",
|
||||
"modelsDevStats": "Sync Statistics",
|
||||
"modelsDevStatsDesc": "Current models.dev data coverage",
|
||||
"modelsWithPricing": "Models with Pricing",
|
||||
"capabilities": "Capabilities",
|
||||
"lastSyncCount": "Last Sync Count",
|
||||
"lastSyncFull": "Last full sync",
|
||||
"modelsDevInfo": "How It Works",
|
||||
"modelsDevInfoDesc": "models.dev is an open-source database of AI model specifications maintained by the SST/OpenCode team. It provides pricing, capabilities, context limits, and modalities for 4,000+ models across 100+ providers.",
|
||||
"modelsDevInfoResolution": "Pricing resolution order (highest priority first):",
|
||||
"modelsDevInfoOrder": "User Override → models.dev → LiteLLM → Hardcoded Default",
|
||||
"memoryTitle": "Memory",
|
||||
"memoryDesc": "Persistent cross-session conversational memory",
|
||||
"memoryEnabled": "Enable Memory",
|
||||
"memoryEnabledDesc": "When enabled, OmniRoute will inject relevant past context.",
|
||||
"maxTokens": "Max Tokens",
|
||||
"retentionDays": "Retention",
|
||||
"recent": "Recent",
|
||||
"recentDesc": "Chronological window",
|
||||
"semantic": "Semantic",
|
||||
"semanticDesc": "Vector search",
|
||||
"hybrid": "Hybrid",
|
||||
"hybridDesc": "Recent + Semantic",
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDesc": "Tools for models",
|
||||
"skillsEnabled": "Enable Skills",
|
||||
"skillsEnabledDesc": "Allows agents to trigger functions.",
|
||||
"skillsComingSoon": "Skills marketplace coming soon.",
|
||||
"memorySkillsTitle": "Memory & Skills",
|
||||
"memorySkillsDesc": "Persistent context & capabilities",
|
||||
"days": "Days"
|
||||
},
|
||||
"translator": {
|
||||
"title": "翻译者",
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function getPricing() {
|
||||
const { getDefaultPricing } = await import("@/shared/constants/pricing");
|
||||
const defaultPricing = getDefaultPricing();
|
||||
|
||||
// Layer 2: Synced external pricing (middle priority)
|
||||
// Layer 2: Synced external pricing from LiteLLM (middle-low priority)
|
||||
const syncedRows = db
|
||||
.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing_synced'")
|
||||
.all();
|
||||
@@ -116,7 +116,24 @@ export async function getPricing() {
|
||||
syncedPricing[key] = toRecord(JSON.parse(rawValue)) as PricingModels;
|
||||
}
|
||||
|
||||
// Layer 3: User overrides (highest priority)
|
||||
// Layer 3: Synced pricing from models.dev (middle-high priority)
|
||||
const modelsDevRows = db
|
||||
.prepare("SELECT key, value FROM key_value WHERE namespace = 'models_dev_pricing'")
|
||||
.all();
|
||||
const modelsDevPricing: PricingByProvider = {};
|
||||
for (const row of modelsDevRows) {
|
||||
const record = toRecord(row);
|
||||
const key = typeof record.key === "string" ? record.key : null;
|
||||
const rawValue = typeof record.value === "string" ? record.value : null;
|
||||
if (!key || rawValue === null) continue;
|
||||
try {
|
||||
modelsDevPricing[key] = JSON.parse(rawValue) as PricingModels;
|
||||
} catch {
|
||||
// Corrupted data — skip silently, fallback to lower layers
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 4: User overrides (highest priority)
|
||||
const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing'").all();
|
||||
const userPricing: PricingByProvider = {};
|
||||
for (const row of rows) {
|
||||
@@ -127,7 +144,7 @@ export async function getPricing() {
|
||||
userPricing[key] = toRecord(JSON.parse(rawValue)) as PricingModels;
|
||||
}
|
||||
|
||||
// Merge: defaults → synced → user (each layer overrides the previous)
|
||||
// Merge: defaults → LiteLLM → models.dev → user (each layer overrides the previous)
|
||||
const mergedPricing: PricingByProvider = {};
|
||||
|
||||
// Start with defaults
|
||||
@@ -135,8 +152,8 @@ export async function getPricing() {
|
||||
mergedPricing[provider] = { ...(toRecord(models) as PricingModels) };
|
||||
}
|
||||
|
||||
// Layer synced then user on top (each higher-priority layer overrides)
|
||||
for (const layer of [syncedPricing, userPricing]) {
|
||||
// Layer synced (LiteLLM), then models.dev, then user on top
|
||||
for (const layer of [syncedPricing, modelsDevPricing, userPricing]) {
|
||||
for (const [provider, models] of Object.entries(layer)) {
|
||||
if (!mergedPricing[provider]) {
|
||||
mergedPricing[provider] = { ...models };
|
||||
|
||||
694
src/lib/modelsDevSync.ts
Normal file
694
src/lib/modelsDevSync.ts
Normal file
@@ -0,0 +1,694 @@
|
||||
/**
|
||||
* modelsDevSync.ts — Fetch model specs, pricing, and capabilities from models.dev
|
||||
*
|
||||
* models.dev (https://github.com/anomalyco/models.dev) is an open-source database
|
||||
* of AI model specifications maintained by the SST/OpenCode team (MIT license).
|
||||
*
|
||||
* API: https://models.dev/api.json
|
||||
* - 109 providers, 4,146+ models
|
||||
* - Data: pricing, capabilities, limits, modalities, metadata
|
||||
*
|
||||
* Resolution order (highest → lowest):
|
||||
* 1. User overrides (`pricing` namespace)
|
||||
* 2. models.dev sync (`models_dev_pricing` namespace)
|
||||
* 3. LiteLLM sync (`pricing_synced` namespace)
|
||||
* 4. Hardcoded defaults (`pricing.ts`)
|
||||
*
|
||||
* Opt-in via MODELS_DEV_SYNC_ENABLED=true (default: false).
|
||||
*/
|
||||
|
||||
import { getDbInstance } from "./db/core";
|
||||
import { invalidateDbCache } from "./db/readCache";
|
||||
import { backupDbFile } from "./db/backup";
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────
|
||||
|
||||
type PricingEntry = {
|
||||
input: number;
|
||||
output: number;
|
||||
cached?: number;
|
||||
cache_creation?: number;
|
||||
reasoning?: number;
|
||||
};
|
||||
|
||||
type PricingModels = Record<string, PricingEntry>;
|
||||
type PricingByProvider = Record<string, PricingModels>;
|
||||
|
||||
interface CapabilityEntry {
|
||||
tool_call: boolean | null;
|
||||
reasoning: boolean | null;
|
||||
attachment: boolean | null;
|
||||
structured_output: boolean | null;
|
||||
temperature: boolean | null;
|
||||
modalities_input: string; // JSON array
|
||||
modalities_output: string; // JSON array
|
||||
knowledge_cutoff: string | null;
|
||||
release_date: string | null;
|
||||
last_updated: string | null;
|
||||
status: string | null;
|
||||
family: string | null;
|
||||
open_weights: boolean | null;
|
||||
limit_context: number | null;
|
||||
limit_input: number | null;
|
||||
limit_output: number | null;
|
||||
interleaved_field: string | null;
|
||||
}
|
||||
|
||||
type CapabilitiesByProvider = Record<string, Record<string, CapabilityEntry>>;
|
||||
|
||||
interface SyncStatus {
|
||||
enabled: boolean;
|
||||
lastSync: string | null;
|
||||
lastSyncModelCount: number;
|
||||
lastSyncCapabilityCount: number;
|
||||
nextSync: string | null;
|
||||
intervalMs: number;
|
||||
}
|
||||
|
||||
interface SyncResult {
|
||||
success: boolean;
|
||||
modelCount: number;
|
||||
providerCount: number;
|
||||
capabilityCount: number;
|
||||
dryRun: boolean;
|
||||
data?: { pricing: PricingByProvider; capabilities: CapabilitiesByProvider };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ─── models.dev API types (raw) ──────────────────────────
|
||||
|
||||
interface ModelsDevCost {
|
||||
input?: number;
|
||||
output?: number;
|
||||
reasoning?: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
input_audio?: number;
|
||||
output_audio?: number;
|
||||
}
|
||||
|
||||
interface ModelsDevLimit {
|
||||
context?: number;
|
||||
input?: number;
|
||||
output?: number;
|
||||
}
|
||||
|
||||
interface ModelsDevModalities {
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
}
|
||||
|
||||
interface ModelsDevInterleaved {
|
||||
field?: string;
|
||||
}
|
||||
|
||||
interface ModelsDevModel {
|
||||
id: string;
|
||||
name: string;
|
||||
family?: string;
|
||||
attachment?: boolean;
|
||||
reasoning?: boolean;
|
||||
tool_call?: boolean;
|
||||
structured_output?: boolean;
|
||||
temperature?: boolean;
|
||||
knowledge?: string;
|
||||
release_date?: string;
|
||||
last_updated?: string;
|
||||
open_weights?: boolean;
|
||||
status?: string;
|
||||
cost?: ModelsDevCost;
|
||||
limit?: ModelsDevLimit;
|
||||
modalities?: ModelsDevModalities;
|
||||
interleaved?: ModelsDevInterleaved | boolean;
|
||||
}
|
||||
|
||||
interface ModelsDevProvider {
|
||||
id: string;
|
||||
name?: string;
|
||||
env?: string[];
|
||||
npm?: string;
|
||||
api?: string;
|
||||
doc?: string;
|
||||
models: Record<string, ModelsDevModel>;
|
||||
}
|
||||
|
||||
type ModelsDevData = Record<string, ModelsDevProvider>;
|
||||
|
||||
// ─── Configuration ───────────────────────────────────────
|
||||
|
||||
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
||||
|
||||
const parsedInterval = parseInt(process.env.MODELS_DEV_SYNC_INTERVAL || "86400", 10);
|
||||
const SYNC_INTERVAL_MS =
|
||||
Number.isFinite(parsedInterval) && parsedInterval > 0 ? parsedInterval * 1000 : 86400 * 1000;
|
||||
|
||||
// ─── Provider mapping: models.dev provider ID → OmniRoute provider IDs/aliases ──
|
||||
//
|
||||
// models.dev uses canonical provider IDs (e.g. "openai", "anthropic", "google").
|
||||
// OmniRoute uses both full IDs and short aliases (e.g. "cc" for claude, "cx" for codex).
|
||||
// We map each models.dev provider to ALL OmniRoute identifiers that should receive
|
||||
// its pricing/capability data.
|
||||
|
||||
const MODELS_DEV_PROVIDER_MAP: Record<string, string[]> = {
|
||||
// Major providers
|
||||
openai: ["openai", "cx"], // cx = Codex (uses OpenAI models)
|
||||
anthropic: ["anthropic", "cc"], // cc = Claude Code
|
||||
google: ["gemini", "gemini-cli"],
|
||||
vertex_ai: ["gemini", "vertex"],
|
||||
deepseek: ["deepseek", "if"], // if = Qoder (routes through DeepSeek)
|
||||
groq: ["groq"],
|
||||
xai: ["xai"],
|
||||
mistral: ["mistral"],
|
||||
together_ai: ["together", "openrouter"],
|
||||
fireworks: ["fireworks"],
|
||||
cerebras: ["cerebras"],
|
||||
cohere: ["cohere"],
|
||||
nvidia: ["nvidia"],
|
||||
nebius: ["nebius"],
|
||||
siliconflow: ["siliconflow"],
|
||||
hyperbolic: ["hyperbolic"],
|
||||
huggingface: ["hf", "huggingface"],
|
||||
openrouter: ["openrouter"],
|
||||
perplexity: ["pplx", "perplexity"],
|
||||
// OAuth / special providers
|
||||
bedrock: ["kiro", "kr"], // kr = Kiro (AWS Bedrock)
|
||||
// Additional providers that may overlap with OmniRoute
|
||||
alibaba: ["ali", "alibaba", "bcp", "alicode", "alicode-intl"],
|
||||
zai: ["zai", "glm"], // GLM models via Z.AI
|
||||
moonshot: ["kimi", "kimi-coding", "kmc", "kmca"],
|
||||
minimax: ["minimax", "minimax-cn"],
|
||||
longcat: ["lc", "longcat"],
|
||||
pollinations: ["pol", "pollinations"],
|
||||
puter: ["pu", "puter"],
|
||||
cloudflare: ["cf"],
|
||||
scaleway: ["scw"],
|
||||
ollama: ["ollamacloud", "ollama-cloud"],
|
||||
blackbox: ["bb", "blackbox"],
|
||||
kilocode: ["kc", "kilocode"],
|
||||
cline: ["cl", "cline"],
|
||||
cursor: ["cu", "cursor"],
|
||||
github: ["gh", "github"],
|
||||
// Fallback: if no mapping exists, use the models.dev ID as-is
|
||||
};
|
||||
|
||||
/**
|
||||
* Map a models.dev provider ID to OmniRoute provider IDs.
|
||||
* Returns array of provider identifiers (may include aliases).
|
||||
*/
|
||||
export function mapProviderId(modelsDevProviderId: string): string[] {
|
||||
return MODELS_DEV_PROVIDER_MAP[modelsDevProviderId] || [modelsDevProviderId];
|
||||
}
|
||||
|
||||
// ─── Periodic sync state ─────────────────────────────────
|
||||
|
||||
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let lastSyncTime: string | null = null;
|
||||
let lastSyncModelCount = 0;
|
||||
let lastSyncCapabilityCount = 0;
|
||||
let activeSyncIntervalMs = SYNC_INTERVAL_MS;
|
||||
let cachedData: ModelsDevData | null = null;
|
||||
let cacheTime = 0;
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
// ─── Core: Fetch ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch raw data from models.dev API.
|
||||
* Uses in-memory cache with 24h TTL to avoid repeated fetches.
|
||||
*/
|
||||
export async function fetchModelsDev(): Promise<ModelsDevData> {
|
||||
// Return cached data if still fresh
|
||||
if (cachedData && Date.now() - cacheTime < CACHE_TTL_MS) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
const response = await fetch(MODELS_DEV_API_URL, {
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`models.dev fetch failed [${response.status}]: ${response.statusText}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
try {
|
||||
const data = JSON.parse(text) as ModelsDevData;
|
||||
cachedData = data;
|
||||
cacheTime = Date.now();
|
||||
return data;
|
||||
} catch {
|
||||
throw new Error(`models.dev returned invalid JSON (${text.slice(0, 100)}...)`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Transform: Pricing ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Transform models.dev raw data → OmniRoute PricingByProvider format.
|
||||
*
|
||||
* models.dev costs are already in $/1M tokens (same as OmniRoute format).
|
||||
* Maps: cache_read → cached, cache_write → cache_creation.
|
||||
*/
|
||||
export function transformModelsDevToPricing(raw: ModelsDevData): PricingByProvider {
|
||||
const result: PricingByProvider = {};
|
||||
|
||||
for (const [providerId, providerData] of Object.entries(raw)) {
|
||||
const omniRouteProviders = mapProviderId(providerId);
|
||||
|
||||
for (const [modelId, model] of Object.entries(providerData.models || {})) {
|
||||
if (!model.cost) continue;
|
||||
|
||||
// Must have at least input pricing
|
||||
if (model.cost.input == null) continue;
|
||||
|
||||
const entry: PricingEntry = {
|
||||
input: model.cost.input,
|
||||
output: model.cost.output ?? 0,
|
||||
};
|
||||
|
||||
if (model.cost.cache_read != null) {
|
||||
entry.cached = model.cost.cache_read;
|
||||
}
|
||||
if (model.cost.cache_write != null) {
|
||||
entry.cache_creation = model.cost.cache_write;
|
||||
}
|
||||
if (model.cost.reasoning != null) {
|
||||
entry.reasoning = model.cost.reasoning;
|
||||
}
|
||||
|
||||
// Write to ALL mapped OmniRoute providers
|
||||
for (const omniProvider of omniRouteProviders) {
|
||||
if (!result[omniProvider]) result[omniProvider] = {};
|
||||
result[omniProvider][modelId] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Transform: Capabilities ─────────────────────────────
|
||||
|
||||
/**
|
||||
* Transform models.dev raw data → CapabilitiesByProvider format.
|
||||
*/
|
||||
export function transformModelsDevToCapabilities(raw: ModelsDevData): CapabilitiesByProvider {
|
||||
const result: CapabilitiesByProvider = {};
|
||||
|
||||
for (const [providerId, providerData] of Object.entries(raw)) {
|
||||
const omniRouteProviders = mapProviderId(providerId);
|
||||
|
||||
for (const [modelId, model] of Object.entries(providerData.models || {})) {
|
||||
const cap: CapabilityEntry = {
|
||||
tool_call: model.tool_call ?? null,
|
||||
reasoning: model.reasoning ?? null,
|
||||
attachment: model.attachment ?? null,
|
||||
structured_output: model.structured_output ?? null,
|
||||
temperature: model.temperature ?? null,
|
||||
modalities_input: JSON.stringify(model.modalities?.input ?? []),
|
||||
modalities_output: JSON.stringify(model.modalities?.output ?? []),
|
||||
knowledge_cutoff: model.knowledge ?? null,
|
||||
release_date: model.release_date ?? null,
|
||||
last_updated: model.last_updated ?? null,
|
||||
status: model.status ?? null,
|
||||
family: model.family ?? null,
|
||||
open_weights: model.open_weights ?? null,
|
||||
limit_context: model.limit?.context ?? null,
|
||||
limit_input: model.limit?.input ?? null,
|
||||
limit_output: model.limit?.output ?? null,
|
||||
interleaved_field:
|
||||
typeof model.interleaved === "object" && model.interleaved?.field
|
||||
? model.interleaved.field
|
||||
: model.interleaved === true
|
||||
? "reasoning_content"
|
||||
: null,
|
||||
};
|
||||
|
||||
for (const omniProvider of omniRouteProviders) {
|
||||
if (!result[omniProvider]) result[omniProvider] = {};
|
||||
result[omniProvider][modelId] = cap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── DB: models.dev pricing namespace ────────────────────
|
||||
|
||||
function toRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read synced pricing from `models_dev_pricing` namespace.
|
||||
*/
|
||||
export function getModelsDevPricing(): PricingByProvider {
|
||||
const db = getDbInstance();
|
||||
const rows = db
|
||||
.prepare("SELECT key, value FROM key_value WHERE namespace = 'models_dev_pricing'")
|
||||
.all();
|
||||
const synced: PricingByProvider = {};
|
||||
for (const row of rows) {
|
||||
const record = toRecord(row);
|
||||
const key = typeof record.key === "string" ? record.key : null;
|
||||
const rawValue = typeof record.value === "string" ? record.value : null;
|
||||
if (!key || rawValue === null) continue;
|
||||
try {
|
||||
synced[key] = JSON.parse(rawValue) as PricingModels;
|
||||
} catch {
|
||||
console.warn(`[MODELS_DEV] Corrupted pricing data for provider "${key}", skipping`);
|
||||
}
|
||||
}
|
||||
return synced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save synced pricing to `models_dev_pricing` namespace (full replace).
|
||||
*/
|
||||
export function saveModelsDevPricing(data: PricingByProvider): void {
|
||||
const db = getDbInstance();
|
||||
const del = db.prepare("DELETE FROM key_value WHERE namespace = 'models_dev_pricing'");
|
||||
const insert = db.prepare(
|
||||
"INSERT INTO key_value (namespace, key, value) VALUES ('models_dev_pricing', ?, ?)"
|
||||
);
|
||||
const tx = db.transaction(() => {
|
||||
del.run();
|
||||
for (const [provider, models] of Object.entries(data)) {
|
||||
insert.run(provider, JSON.stringify(models));
|
||||
}
|
||||
});
|
||||
tx();
|
||||
backupDbFile("pre-write");
|
||||
invalidateDbCache("pricing");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all models.dev synced pricing data.
|
||||
*/
|
||||
export function clearModelsDevPricing(): void {
|
||||
const db = getDbInstance();
|
||||
db.prepare("DELETE FROM key_value WHERE namespace = 'models_dev_pricing'").run();
|
||||
backupDbFile("pre-write");
|
||||
invalidateDbCache("pricing");
|
||||
}
|
||||
|
||||
// ─── DB: model_capabilities table ────────────────────────
|
||||
|
||||
/**
|
||||
* Ensure the model_capabilities table exists.
|
||||
* Call this before any capability operations.
|
||||
*/
|
||||
export function ensureCapabilitiesTable(): void {
|
||||
const db = getDbInstance();
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS model_capabilities (
|
||||
provider TEXT NOT NULL,
|
||||
model_id TEXT NOT NULL,
|
||||
tool_call BOOLEAN,
|
||||
reasoning BOOLEAN,
|
||||
attachment BOOLEAN,
|
||||
structured_output BOOLEAN,
|
||||
temperature BOOLEAN,
|
||||
modalities_input TEXT,
|
||||
modalities_output TEXT,
|
||||
knowledge_cutoff TEXT,
|
||||
release_date TEXT,
|
||||
last_updated TEXT,
|
||||
status TEXT,
|
||||
family TEXT,
|
||||
open_weights BOOLEAN,
|
||||
limit_context INTEGER,
|
||||
limit_input INTEGER,
|
||||
limit_output INTEGER,
|
||||
interleaved_field TEXT,
|
||||
last_synced TEXT,
|
||||
PRIMARY KEY (provider, model_id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read synced capabilities from `model_capabilities` table.
|
||||
*/
|
||||
export function getSyncedCapabilities(
|
||||
provider?: string,
|
||||
modelId?: string
|
||||
): Record<string, Record<string, CapabilityEntry>> {
|
||||
const db = getDbInstance();
|
||||
ensureCapabilitiesTable();
|
||||
|
||||
let query = "SELECT * FROM model_capabilities";
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
if (provider) {
|
||||
query += " WHERE provider = ?";
|
||||
params.push(provider);
|
||||
if (modelId) {
|
||||
query += " AND model_id = ?";
|
||||
params.push(modelId);
|
||||
}
|
||||
}
|
||||
|
||||
const rows = db.prepare(query).all(...params);
|
||||
const result: Record<string, Record<string, CapabilityEntry>> = {};
|
||||
|
||||
for (const row of rows) {
|
||||
const record = toRecord(row);
|
||||
const prov = typeof record.provider === "string" ? record.provider : null;
|
||||
const mid = typeof record.model_id === "string" ? record.model_id : null;
|
||||
if (!prov || !mid) continue;
|
||||
|
||||
if (!result[prov]) result[prov] = {};
|
||||
result[prov][mid] = {
|
||||
tool_call: record.tool_call === 1 ? true : record.tool_call === 0 ? false : null,
|
||||
reasoning: record.reasoning === 1 ? true : record.reasoning === 0 ? false : null,
|
||||
attachment: record.attachment === 1 ? true : record.attachment === 0 ? false : null,
|
||||
structured_output:
|
||||
record.structured_output === 1 ? true : record.structured_output === 0 ? false : null,
|
||||
temperature: record.temperature === 1 ? true : record.temperature === 0 ? false : null,
|
||||
modalities_input:
|
||||
typeof record.modalities_input === "string" ? record.modalities_input : "[]",
|
||||
modalities_output:
|
||||
typeof record.modalities_output === "string" ? record.modalities_output : "[]",
|
||||
knowledge_cutoff:
|
||||
typeof record.knowledge_cutoff === "string" ? record.knowledge_cutoff : null,
|
||||
release_date: typeof record.release_date === "string" ? record.release_date : null,
|
||||
last_updated: typeof record.last_updated === "string" ? record.last_updated : null,
|
||||
status: typeof record.status === "string" ? record.status : null,
|
||||
family: typeof record.family === "string" ? record.family : null,
|
||||
open_weights: record.open_weights === 1 ? true : record.open_weights === 0 ? false : null,
|
||||
limit_context: typeof record.limit_context === "number" ? record.limit_context : null,
|
||||
limit_input: typeof record.limit_input === "number" ? record.limit_input : null,
|
||||
limit_output: typeof record.limit_output === "number" ? record.limit_output : null,
|
||||
interleaved_field:
|
||||
typeof record.interleaved_field === "string" ? record.interleaved_field : null,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save synced capabilities to `model_capabilities` table (full replace).
|
||||
*/
|
||||
export function saveModelsDevCapabilities(data: CapabilitiesByProvider): void {
|
||||
const db = getDbInstance();
|
||||
ensureCapabilitiesTable();
|
||||
|
||||
const del = db.prepare("DELETE FROM model_capabilities");
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO model_capabilities (
|
||||
provider, model_id, tool_call, reasoning, attachment, structured_output,
|
||||
temperature, modalities_input, modalities_output, knowledge_cutoff,
|
||||
release_date, last_updated, status, family, open_weights,
|
||||
limit_context, limit_input, limit_output, interleaved_field, last_synced
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const tx = db.transaction(() => {
|
||||
del.run();
|
||||
for (const [provider, models] of Object.entries(data)) {
|
||||
for (const [modelId, cap] of Object.entries(models)) {
|
||||
insert.run(
|
||||
provider,
|
||||
modelId,
|
||||
cap.tool_call === null ? null : cap.tool_call ? 1 : 0,
|
||||
cap.reasoning === null ? null : cap.reasoning ? 1 : 0,
|
||||
cap.attachment === null ? null : cap.attachment ? 1 : 0,
|
||||
cap.structured_output === null ? null : cap.structured_output ? 1 : 0,
|
||||
cap.temperature === null ? null : cap.temperature ? 1 : 0,
|
||||
cap.modalities_input,
|
||||
cap.modalities_output,
|
||||
cap.knowledge_cutoff,
|
||||
cap.release_date,
|
||||
cap.last_updated,
|
||||
cap.status,
|
||||
cap.family,
|
||||
cap.open_weights === null ? null : cap.open_weights ? 1 : 0,
|
||||
cap.limit_context,
|
||||
cap.limit_input,
|
||||
cap.limit_output,
|
||||
cap.interleaved_field,
|
||||
now
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
tx();
|
||||
backupDbFile("pre-write");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all synced capability data.
|
||||
*/
|
||||
export function clearModelsDevCapabilities(): void {
|
||||
const db = getDbInstance();
|
||||
ensureCapabilitiesTable();
|
||||
db.prepare("DELETE FROM model_capabilities").run();
|
||||
backupDbFile("pre-write");
|
||||
}
|
||||
|
||||
// ─── Main sync function ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch, transform, and save pricing + capabilities from models.dev.
|
||||
*/
|
||||
export async function syncModelsDev(opts?: {
|
||||
dryRun?: boolean;
|
||||
syncCapabilities?: boolean;
|
||||
}): Promise<SyncResult> {
|
||||
const dryRun = opts?.dryRun ?? false;
|
||||
const syncCapabilities = opts?.syncCapabilities ?? true;
|
||||
|
||||
try {
|
||||
const raw = await fetchModelsDev();
|
||||
const pricing = transformModelsDevToPricing(raw);
|
||||
const capabilities = syncCapabilities ? transformModelsDevToCapabilities(raw) : {};
|
||||
|
||||
const modelCount = Object.values(pricing).reduce(
|
||||
(sum, models) => sum + Object.keys(models).length,
|
||||
0
|
||||
);
|
||||
const providerCount = Object.keys(pricing).length;
|
||||
const capabilityCount = syncCapabilities
|
||||
? Object.values(capabilities).reduce((sum, models) => sum + Object.keys(models).length, 0)
|
||||
: 0;
|
||||
|
||||
if (!dryRun) {
|
||||
saveModelsDevPricing(pricing);
|
||||
if (syncCapabilities) {
|
||||
ensureCapabilitiesTable();
|
||||
saveModelsDevCapabilities(capabilities);
|
||||
}
|
||||
lastSyncTime = new Date().toISOString();
|
||||
lastSyncModelCount = modelCount;
|
||||
lastSyncCapabilityCount = capabilityCount;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
modelCount,
|
||||
providerCount,
|
||||
capabilityCount,
|
||||
dryRun,
|
||||
...(dryRun ? { data: { pricing, capabilities } } : {}),
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.warn("[MODELS_DEV] Sync failed:", message);
|
||||
return {
|
||||
success: false,
|
||||
modelCount: 0,
|
||||
providerCount: 0,
|
||||
capabilityCount: 0,
|
||||
dryRun,
|
||||
error: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Periodic sync ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Start periodic models.dev sync (non-blocking).
|
||||
*/
|
||||
export function startPeriodicSync(intervalMs?: number): void {
|
||||
if (syncTimer) return; // Already running
|
||||
|
||||
const interval = intervalMs ?? SYNC_INTERVAL_MS;
|
||||
activeSyncIntervalMs = interval;
|
||||
console.log(`[MODELS_DEV] Starting periodic sync every ${interval / 1000}s`);
|
||||
|
||||
// Initial sync (non-blocking)
|
||||
syncModelsDev()
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
console.log(
|
||||
`[MODELS_DEV] Initial sync complete: ${result.modelCount} pricing entries, ${result.capabilityCount} capabilities from ${result.providerCount} providers`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("[MODELS_DEV] Initial sync error:", err instanceof Error ? err.message : err);
|
||||
});
|
||||
|
||||
syncTimer = setInterval(() => {
|
||||
syncModelsDev()
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
console.log(`[MODELS_DEV] Periodic sync complete: ${result.modelCount} pricing entries`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("[MODELS_DEV] Periodic sync error:", err instanceof Error ? err.message : err);
|
||||
});
|
||||
}, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop periodic sync and cleanup timer.
|
||||
*/
|
||||
export function stopPeriodicSync(): void {
|
||||
if (syncTimer) {
|
||||
clearInterval(syncTimer);
|
||||
syncTimer = null;
|
||||
console.log("[MODELS_DEV] Periodic sync stopped");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current sync status.
|
||||
*/
|
||||
export function getSyncStatus(): SyncStatus {
|
||||
// If the sync timer is active, it's enabled.
|
||||
const enabled = syncTimer !== null;
|
||||
return {
|
||||
enabled,
|
||||
lastSync: lastSyncTime,
|
||||
lastSyncModelCount,
|
||||
lastSyncCapabilityCount,
|
||||
nextSync:
|
||||
syncTimer && lastSyncTime
|
||||
? new Date(new Date(lastSyncTime).getTime() + activeSyncIntervalMs).toISOString()
|
||||
: null,
|
||||
intervalMs: activeSyncIntervalMs,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Init (called from server-init.ts) ───────────────────
|
||||
|
||||
/**
|
||||
* Initialize models.dev sync if enabled.
|
||||
*/
|
||||
export async function initModelsDevSync(): Promise<void> {
|
||||
const { getSettings } = await import("./localDb");
|
||||
const settings = await getSettings();
|
||||
|
||||
if (settings.modelsDevSyncEnabled !== true) {
|
||||
console.log("[MODELS_DEV] Disabled (enable via Settings > AI)");
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = settings.modelsDevSyncInterval as number | undefined;
|
||||
startPeriodicSync(interval);
|
||||
}
|
||||
@@ -72,4 +72,7 @@ export const updateSettingsSchema = z.object({
|
||||
.optional(),
|
||||
// SkillsMP marketplace API key
|
||||
skillsmpApiKey: z.string().max(200).optional(),
|
||||
// models.dev sync settings
|
||||
modelsDevSyncEnabled: z.boolean().optional(),
|
||||
modelsDevSyncInterval: z.number().int().min(3600).max(604800).optional(),
|
||||
});
|
||||
|
||||
186
tests/integration/modelsDevSync.test.mjs
Normal file
186
tests/integration/modelsDevSync.test.mjs
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Integration tests for models.dev sync: live fetch → DB save → retrieve → resolution order.
|
||||
*
|
||||
* Uses a test DB file in /tmp to avoid touching production data.
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { unlinkSync, existsSync } from "node:fs";
|
||||
|
||||
// Point DB at a temp file
|
||||
const TEST_DB = "/tmp/omniroute-test-modelsdev.sqlite";
|
||||
|
||||
before(() => {
|
||||
if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
|
||||
process.env.DATA_DIR = "/tmp";
|
||||
process.env.DB_FILE = TEST_DB;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (existsSync(TEST_DB)) unlinkSync(TEST_DB);
|
||||
});
|
||||
|
||||
describe("modelsDevSync — integration: live fetch → DB → retrieve", () => {
|
||||
it("fetches, saves pricing, and retrieves from DB", async () => {
|
||||
const {
|
||||
syncModelsDev,
|
||||
getModelsDevPricing,
|
||||
getSyncedCapabilities,
|
||||
clearModelsDevPricing,
|
||||
clearModelsDevCapabilities,
|
||||
} = await import("../../src/lib/modelsDevSync.ts");
|
||||
|
||||
// 1. Sync from live API
|
||||
const result = await syncModelsDev({ dryRun: false, syncCapabilities: true });
|
||||
assert.equal(result.success, true, `sync should succeed: ${result.error || ""}`);
|
||||
assert.ok(result.modelCount > 0, `should have pricing entries, got ${result.modelCount}`);
|
||||
assert.ok(result.providerCount > 0, `should have providers, got ${result.providerCount}`);
|
||||
assert.ok(
|
||||
result.capabilityCount > 0,
|
||||
`should have capabilities, got ${result.capabilityCount}`
|
||||
);
|
||||
|
||||
// 2. Retrieve pricing from DB
|
||||
const pricing = getModelsDevPricing();
|
||||
assert.ok(Object.keys(pricing).length > 0, "should have pricing in DB");
|
||||
|
||||
// 3. Verify specific known models
|
||||
assert.ok(pricing.openai, "openai pricing should exist");
|
||||
assert.ok(pricing.openai["gpt-4o"], "gpt-4o pricing should exist in openai");
|
||||
assert.ok(pricing.openai["gpt-4o"].input > 0, "gpt-4o input price should be > 0");
|
||||
assert.ok(pricing.openai["gpt-4o"].output > 0, "gpt-4o output price should be > 0");
|
||||
|
||||
assert.ok(pricing.anthropic, "anthropic pricing should exist");
|
||||
const claudeModels = Object.keys(pricing.anthropic).filter((m) => m.includes("claude"));
|
||||
assert.ok(claudeModels.length > 0, "should have claude pricing entries");
|
||||
|
||||
assert.ok(pricing.deepseek, "deepseek pricing should exist");
|
||||
assert.ok(pricing.if, "if (Qoder) pricing should exist (mapped from deepseek)");
|
||||
|
||||
// 4. Retrieve capabilities from DB
|
||||
const caps = getSyncedCapabilities();
|
||||
assert.ok(Object.keys(caps).length > 0, "should have capabilities in DB");
|
||||
|
||||
assert.ok(caps.openai, "openai capabilities should exist");
|
||||
assert.ok(caps.openai["gpt-4o"], "gpt-4o capabilities should exist");
|
||||
assert.equal(caps.openai["gpt-4o"].tool_call, true);
|
||||
assert.equal(caps.openai["gpt-4o"].reasoning, false);
|
||||
assert.equal(caps.openai["gpt-4o"].attachment, true);
|
||||
|
||||
// 5. Query specific capability
|
||||
const specificCaps = getSyncedCapabilities("openai", "gpt-4o");
|
||||
assert.ok(specificCaps.openai, "specific query should return openai");
|
||||
assert.ok(specificCaps.openai["gpt-4o"], "specific query should return gpt-4o");
|
||||
assert.equal(specificCaps.openai["gpt-4o"].limit_context, 128000);
|
||||
|
||||
// 6. Cleanup
|
||||
clearModelsDevPricing();
|
||||
clearModelsDevCapabilities();
|
||||
|
||||
const afterPricing = getModelsDevPricing();
|
||||
assert.equal(Object.keys(afterPricing).length, 0, "pricing should be cleared");
|
||||
|
||||
const afterCaps = getSyncedCapabilities();
|
||||
assert.equal(Object.keys(afterCaps).length, 0, "capabilities should be cleared");
|
||||
});
|
||||
|
||||
it("dryRun returns data without saving to DB", async () => {
|
||||
const { syncModelsDev, getModelsDevPricing, clearModelsDevPricing } =
|
||||
await import("../../src/lib/modelsDevSync.ts");
|
||||
|
||||
// Clear any existing data first
|
||||
clearModelsDevPricing();
|
||||
|
||||
const result = await syncModelsDev({ dryRun: true, syncCapabilities: false });
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(result.data, "dryRun should return data");
|
||||
assert.ok(result.data.pricing, "dryRun should return pricing data");
|
||||
|
||||
// Verify nothing was saved
|
||||
const pricing = getModelsDevPricing();
|
||||
assert.equal(Object.keys(pricing).length, 0, "dryRun should not save to DB");
|
||||
});
|
||||
|
||||
it("sync without capabilities only saves pricing", async () => {
|
||||
const {
|
||||
syncModelsDev,
|
||||
getModelsDevPricing,
|
||||
getSyncedCapabilities,
|
||||
clearModelsDevPricing,
|
||||
clearModelsDevCapabilities,
|
||||
} = await import("../../src/lib/modelsDevSync.ts");
|
||||
|
||||
clearModelsDevPricing();
|
||||
clearModelsDevCapabilities();
|
||||
|
||||
const result = await syncModelsDev({ dryRun: false, syncCapabilities: false });
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(result.modelCount > 0, "should have pricing entries");
|
||||
assert.equal(result.capabilityCount, 0, "capabilityCount should be 0 when disabled");
|
||||
|
||||
// Pricing should be saved
|
||||
const pricing = getModelsDevPricing();
|
||||
assert.ok(Object.keys(pricing).length > 0, "pricing should be saved");
|
||||
|
||||
// Capabilities should NOT be saved
|
||||
const caps = getSyncedCapabilities();
|
||||
assert.equal(Object.keys(caps).length, 0, "capabilities should not be saved");
|
||||
|
||||
clearModelsDevPricing();
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelsDevSync — resolution order: user > models.dev > LiteLLM > default", () => {
|
||||
it("returns models.dev pricing when no user override exists", async () => {
|
||||
const { syncModelsDev, getModelsDevPricing, clearModelsDevPricing } =
|
||||
await import("../../src/lib/modelsDevSync.ts");
|
||||
|
||||
clearModelsDevPricing();
|
||||
await syncModelsDev({ dryRun: false, syncCapabilities: false });
|
||||
|
||||
const pricing = getModelsDevPricing();
|
||||
assert.ok(pricing.openai, "openai pricing should exist");
|
||||
assert.ok(pricing.openai["gpt-4o"], "gpt-4o should have models.dev pricing");
|
||||
|
||||
clearModelsDevPricing();
|
||||
});
|
||||
|
||||
it("getPricing() merges all layers correctly", async () => {
|
||||
const { getPricing } = await import("../../src/lib/db/settings.ts");
|
||||
const { syncModelsDev, clearModelsDevPricing, saveModelsDevPricing } =
|
||||
await import("../../src/lib/modelsDevSync.ts");
|
||||
const { updatePricing, resetPricing } = await import("../../src/lib/db/settings.ts");
|
||||
|
||||
// Clear all synced data first
|
||||
clearModelsDevPricing();
|
||||
|
||||
// Sync models.dev pricing
|
||||
await syncModelsDev({ dryRun: false, syncCapabilities: false });
|
||||
|
||||
// Get merged pricing
|
||||
const merged = await getPricing();
|
||||
|
||||
// Should have openai pricing (either from models.dev or hardcoded defaults)
|
||||
assert.ok(merged.openai || merged.cx, "should have openai/cx pricing in merged result");
|
||||
|
||||
// Add a user override
|
||||
await updatePricing({
|
||||
openai: {
|
||||
"gpt-4o": { input: 999, output: 999 },
|
||||
},
|
||||
});
|
||||
|
||||
// Verify user override wins
|
||||
const mergedWithOverride = await getPricing();
|
||||
assert.equal(
|
||||
mergedWithOverride.openai?.["gpt-4o"]?.input,
|
||||
999,
|
||||
"user override should win over models.dev"
|
||||
);
|
||||
|
||||
// Reset the override
|
||||
await resetPricing("openai", "gpt-4o");
|
||||
clearModelsDevPricing();
|
||||
});
|
||||
});
|
||||
449
tests/unit/modelsDevSync.test.mjs
Normal file
449
tests/unit/modelsDevSync.test.mjs
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* Unit tests for modelsDevSync.ts
|
||||
*
|
||||
* Tests: fetch, transform (pricing + capabilities), provider mapping,
|
||||
* DB save/retrieve, and resolution order.
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
transformModelsDevToPricing,
|
||||
transformModelsDevToCapabilities,
|
||||
mapProviderId,
|
||||
fetchModelsDev,
|
||||
} from "../../src/lib/modelsDevSync.ts";
|
||||
|
||||
// ─── Mock data ───────────────────────────────────────────
|
||||
|
||||
const MOCK_MODELS_DEV_DATA = {
|
||||
openai: {
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
npm: "@ai-sdk/openai",
|
||||
env: ["OPENAI_API_KEY"],
|
||||
doc: "https://platform.openai.com/docs/models",
|
||||
models: {
|
||||
"gpt-4o": {
|
||||
id: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
family: "gpt-4",
|
||||
attachment: true,
|
||||
reasoning: false,
|
||||
tool_call: true,
|
||||
structured_output: true,
|
||||
temperature: true,
|
||||
knowledge: "2024-10",
|
||||
release_date: "2024-05-13",
|
||||
last_updated: "2024-10-01",
|
||||
open_weights: false,
|
||||
cost: {
|
||||
input: 2.5,
|
||||
output: 10.0,
|
||||
cache_read: 1.25,
|
||||
cache_write: 2.5,
|
||||
},
|
||||
limit: {
|
||||
context: 128000,
|
||||
input: 128000,
|
||||
output: 16384,
|
||||
},
|
||||
modalities: {
|
||||
input: ["text", "image"],
|
||||
output: ["text"],
|
||||
},
|
||||
},
|
||||
"o3-mini": {
|
||||
id: "o3-mini",
|
||||
name: "o3-mini",
|
||||
family: "o3",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
structured_output: true,
|
||||
temperature: false,
|
||||
release_date: "2025-01-31",
|
||||
last_updated: "2025-01-31",
|
||||
open_weights: false,
|
||||
cost: {
|
||||
input: 1.1,
|
||||
output: 4.4,
|
||||
reasoning: 4.4,
|
||||
},
|
||||
limit: {
|
||||
context: 200000,
|
||||
output: 100000,
|
||||
},
|
||||
modalities: {
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
anthropic: {
|
||||
id: "anthropic",
|
||||
name: "Anthropic",
|
||||
npm: "@ai-sdk/anthropic",
|
||||
env: ["ANTHROPIC_API_KEY"],
|
||||
doc: "https://docs.anthropic.com/en/docs/about-claud/models",
|
||||
models: {
|
||||
"claude-sonnet-4-20250514": {
|
||||
id: "claude-sonnet-4-20250514",
|
||||
name: "Claude Sonnet 4",
|
||||
family: "claude-4",
|
||||
attachment: true,
|
||||
reasoning: false,
|
||||
tool_call: true,
|
||||
structured_output: true,
|
||||
temperature: true,
|
||||
release_date: "2025-05-14",
|
||||
last_updated: "2025-05-14",
|
||||
open_weights: false,
|
||||
cost: {
|
||||
input: 3.0,
|
||||
output: 15.0,
|
||||
cache_read: 0.3,
|
||||
cache_write: 3.75,
|
||||
},
|
||||
limit: {
|
||||
context: 200000,
|
||||
output: 64000,
|
||||
},
|
||||
modalities: {
|
||||
input: ["text", "image"],
|
||||
output: ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
deepseek: {
|
||||
id: "deepseek",
|
||||
name: "DeepSeek",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
api: "https://api.deepseek.com/v1",
|
||||
env: ["DEEPSEEK_API_KEY"],
|
||||
doc: "https://api-docs.deepseek.com/",
|
||||
models: {
|
||||
"deepseek-chat": {
|
||||
id: "deepseek-chat",
|
||||
name: "DeepSeek V3",
|
||||
family: "deepseek-v3",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: true,
|
||||
temperature: true,
|
||||
release_date: "2024-12-26",
|
||||
last_updated: "2025-01-20",
|
||||
open_weights: true,
|
||||
cost: {
|
||||
input: 0.27,
|
||||
output: 1.1,
|
||||
cache_read: 0.07,
|
||||
},
|
||||
limit: {
|
||||
context: 64000,
|
||||
output: 8192,
|
||||
},
|
||||
modalities: {
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"unknown-provider": {
|
||||
id: "unknown-provider",
|
||||
name: "Unknown",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
api: "https://api.unknown.com/v1",
|
||||
env: ["UNKNOWN_API_KEY"],
|
||||
models: {
|
||||
"some-model": {
|
||||
id: "some-model",
|
||||
name: "Some Model",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: false,
|
||||
release_date: "2025-01-01",
|
||||
last_updated: "2025-01-01",
|
||||
open_weights: false,
|
||||
cost: {
|
||||
input: 1.0,
|
||||
output: 2.0,
|
||||
},
|
||||
limit: {
|
||||
context: 32000,
|
||||
output: 4096,
|
||||
},
|
||||
modalities: {
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────
|
||||
|
||||
describe("modelsDevSync — transformModelsDevToPricing", () => {
|
||||
it("transforms pricing data correctly for openai provider", () => {
|
||||
const pricing = transformModelsDevToPricing(MOCK_MODELS_DEV_DATA);
|
||||
|
||||
// openai maps to ["openai", "cx"]
|
||||
assert.ok(pricing.openai, "openai provider should exist");
|
||||
assert.ok(pricing.openai["gpt-4o"], "gpt-4o pricing should exist");
|
||||
assert.equal(pricing.openai["gpt-4o"].input, 2.5);
|
||||
assert.equal(pricing.openai["gpt-4o"].output, 10.0);
|
||||
assert.equal(pricing.openai["gpt-4o"].cached, 1.25);
|
||||
assert.equal(pricing.openai["gpt-4o"].cache_creation, 2.5);
|
||||
|
||||
// cx (Codex alias) should also have the same pricing
|
||||
assert.ok(pricing.cx, "cx provider should exist");
|
||||
assert.ok(pricing.cx["gpt-4o"], "gpt-4o pricing in cx should exist");
|
||||
assert.equal(pricing.cx["gpt-4o"].input, 2.5);
|
||||
});
|
||||
|
||||
it("maps reasoning cost correctly", () => {
|
||||
const pricing = transformModelsDevToPricing(MOCK_MODELS_DEV_DATA);
|
||||
|
||||
assert.ok(pricing.openai["o3-mini"], "o3-mini pricing should exist");
|
||||
assert.equal(pricing.openai["o3-mini"].input, 1.1);
|
||||
assert.equal(pricing.openai["o3-mini"].output, 4.4);
|
||||
assert.equal(pricing.openai["o3-mini"].reasoning, 4.4);
|
||||
// o3-mini has no cache_read/cache_write
|
||||
assert.equal(pricing.openai["o3-mini"].cached, undefined);
|
||||
assert.equal(pricing.openai["o3-mini"].cache_creation, undefined);
|
||||
});
|
||||
|
||||
it("maps deepseek to both deepseek and if (Qoder) providers", () => {
|
||||
const pricing = transformModelsDevToPricing(MOCK_MODELS_DEV_DATA);
|
||||
|
||||
assert.ok(pricing.deepseek, "deepseek provider should exist");
|
||||
assert.ok(pricing.if, "if (Qoder) provider should exist");
|
||||
assert.equal(pricing.deepseek["deepseek-chat"].input, 0.27);
|
||||
assert.equal(pricing.deepseek["deepseek-chat"].output, 1.1);
|
||||
assert.equal(pricing.deepseek["deepseek-chat"].cached, 0.07);
|
||||
// Should be same in if provider
|
||||
assert.equal(pricing.if["deepseek-chat"].input, 0.27);
|
||||
});
|
||||
|
||||
it("handles unknown providers by using the models.dev ID as-is", () => {
|
||||
const pricing = transformModelsDevToPricing(MOCK_MODELS_DEV_DATA);
|
||||
|
||||
assert.ok(pricing["unknown-provider"], "unknown-provider should exist");
|
||||
assert.equal(pricing["unknown-provider"]["some-model"].input, 1.0);
|
||||
assert.equal(pricing["unknown-provider"]["some-model"].output, 2.0);
|
||||
});
|
||||
|
||||
it("skips models without cost data", () => {
|
||||
const dataWithNoCost = {
|
||||
testprovider: {
|
||||
id: "testprovider",
|
||||
models: {
|
||||
"no-cost-model": {
|
||||
id: "no-cost-model",
|
||||
name: "No Cost Model",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: false,
|
||||
release_date: "2025-01-01",
|
||||
last_updated: "2025-01-01",
|
||||
open_weights: false,
|
||||
// No cost field
|
||||
limit: { context: 4096, output: 2048 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const pricing = transformModelsDevToPricing(dataWithNoCost);
|
||||
assert.equal(pricing.testprovider, undefined, "provider without cost should not exist");
|
||||
});
|
||||
|
||||
it("counts pricing entries correctly", () => {
|
||||
const pricing = transformModelsDevToPricing(MOCK_MODELS_DEV_DATA);
|
||||
const totalModels = Object.values(pricing).reduce(
|
||||
(sum, models) => sum + Object.keys(models).length,
|
||||
0
|
||||
);
|
||||
// openai: 2 models × 2 providers (openai, cx) = 4
|
||||
// anthropic: 1 model × 2 providers (anthropic, cc) = 2
|
||||
// deepseek: 1 model × 2 providers (deepseek, if) = 2
|
||||
// unknown-provider: 1 model × 1 provider = 1
|
||||
assert.equal(totalModels, 9);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelsDevSync — transformModelsDevToCapabilities", () => {
|
||||
it("transforms capability data correctly", () => {
|
||||
const caps = transformModelsDevToCapabilities(MOCK_MODELS_DEV_DATA);
|
||||
|
||||
assert.ok(caps.openai, "openai capabilities should exist");
|
||||
assert.ok(caps.openai["gpt-4o"], "gpt-4o capabilities should exist");
|
||||
|
||||
const gpt4o = caps.openai["gpt-4o"];
|
||||
assert.equal(gpt4o.tool_call, true);
|
||||
assert.equal(gpt4o.reasoning, false);
|
||||
assert.equal(gpt4o.attachment, true);
|
||||
assert.equal(gpt4o.structured_output, true);
|
||||
assert.equal(gpt4o.temperature, true);
|
||||
assert.equal(gpt4o.limit_context, 128000);
|
||||
assert.equal(gpt4o.limit_output, 16384);
|
||||
assert.equal(gpt4o.status, null); // not set in mock
|
||||
assert.equal(gpt4o.family, "gpt-4");
|
||||
assert.equal(gpt4o.open_weights, false);
|
||||
assert.equal(JSON.parse(gpt4o.modalities_input).length, 2);
|
||||
assert.equal(JSON.parse(gpt4o.modalities_output).length, 1);
|
||||
});
|
||||
|
||||
it("handles interleaved reasoning field", () => {
|
||||
const dataWithInterleaved = {
|
||||
testprovider: {
|
||||
id: "testprovider",
|
||||
models: {
|
||||
"reasoning-model": {
|
||||
id: "reasoning-model",
|
||||
name: "Reasoning Model",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
release_date: "2025-01-01",
|
||||
last_updated: "2025-01-01",
|
||||
open_weights: true,
|
||||
interleaved: { field: "reasoning_content" },
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const caps = transformModelsDevToCapabilities(dataWithInterleaved);
|
||||
assert.equal(caps.testprovider["reasoning-model"].interleaved_field, "reasoning_content");
|
||||
});
|
||||
|
||||
it("handles interleaved as boolean true", () => {
|
||||
const dataWithBoolInterleaved = {
|
||||
testprovider: {
|
||||
id: "testprovider",
|
||||
models: {
|
||||
"bool-model": {
|
||||
id: "bool-model",
|
||||
name: "Bool Model",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
release_date: "2025-01-01",
|
||||
last_updated: "2025-01-01",
|
||||
open_weights: false,
|
||||
interleaved: true,
|
||||
limit: { context: 100000, output: 32000 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const caps = transformModelsDevToCapabilities(dataWithBoolInterleaved);
|
||||
assert.equal(caps.testprovider["bool-model"].interleaved_field, "reasoning_content");
|
||||
});
|
||||
|
||||
it("handles null/missing optional fields", () => {
|
||||
const dataWithMinimal = {
|
||||
minimal: {
|
||||
id: "minimal",
|
||||
models: {
|
||||
"minimal-model": {
|
||||
id: "minimal-model",
|
||||
name: "Minimal",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: false,
|
||||
release_date: "2025-01-01",
|
||||
last_updated: "2025-01-01",
|
||||
open_weights: false,
|
||||
// No cost, no interleaved, no status, no family
|
||||
limit: { context: 4096, output: 2048 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const caps = transformModelsDevToCapabilities(dataWithMinimal);
|
||||
const m = caps.minimal["minimal-model"];
|
||||
assert.equal(m.tool_call, false);
|
||||
assert.equal(m.reasoning, false);
|
||||
assert.equal(m.attachment, false);
|
||||
assert.equal(m.structured_output, null);
|
||||
assert.equal(m.temperature, null);
|
||||
assert.equal(m.knowledge_cutoff, null);
|
||||
assert.equal(m.status, null);
|
||||
assert.equal(m.family, null);
|
||||
assert.equal(m.interleaved_field, null);
|
||||
assert.equal(m.limit_context, 4096);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelsDevSync — mapProviderId", () => {
|
||||
it("maps openai to [openai, cx]", () => {
|
||||
assert.deepEqual(mapProviderId("openai"), ["openai", "cx"]);
|
||||
});
|
||||
|
||||
it("maps anthropic to [anthropic, cc]", () => {
|
||||
assert.deepEqual(mapProviderId("anthropic"), ["anthropic", "cc"]);
|
||||
});
|
||||
|
||||
it("maps google to [gemini, gemini-cli]", () => {
|
||||
assert.deepEqual(mapProviderId("google"), ["gemini", "gemini-cli"]);
|
||||
});
|
||||
|
||||
it("maps deepseek to [deepseek, if]", () => {
|
||||
assert.deepEqual(mapProviderId("deepseek"), ["deepseek", "if"]);
|
||||
});
|
||||
|
||||
it("falls back to original ID for unmapped providers", () => {
|
||||
assert.deepEqual(mapProviderId("some-new-provider"), ["some-new-provider"]);
|
||||
});
|
||||
|
||||
it("maps bedrock to [kiro, kr]", () => {
|
||||
assert.deepEqual(mapProviderId("bedrock"), ["kiro", "kr"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelsDevSync — fetchModelsDev (live API)", () => {
|
||||
it("fetches data from models.dev API", async () => {
|
||||
const data = await fetchModelsDev();
|
||||
assert.ok(typeof data === "object", "data should be an object");
|
||||
|
||||
const providerCount = Object.keys(data).length;
|
||||
assert.ok(providerCount >= 100, `should have 100+ providers, got ${providerCount}`);
|
||||
|
||||
let modelCount = 0;
|
||||
for (const provider of Object.values(data)) {
|
||||
const p = provider;
|
||||
if (p.models) {
|
||||
modelCount += Object.keys(p.models).length;
|
||||
}
|
||||
}
|
||||
assert.ok(modelCount >= 4000, `should have 4000+ models, got ${modelCount}`);
|
||||
});
|
||||
|
||||
it("returns cached data on second call", async () => {
|
||||
const data1 = await fetchModelsDev();
|
||||
const data2 = await fetchModelsDev();
|
||||
assert.strictEqual(data1, data2, "should return same cached reference");
|
||||
});
|
||||
|
||||
it("has openai provider with gpt-4o model", async () => {
|
||||
const data = await fetchModelsDev();
|
||||
assert.ok(data.openai, "openai provider should exist");
|
||||
assert.ok(data.openai.models["gpt-4o"], "gpt-4o model should exist");
|
||||
});
|
||||
|
||||
it("has anthropic provider with claude models", async () => {
|
||||
const data = await fetchModelsDev();
|
||||
assert.ok(data.anthropic, "anthropic provider should exist");
|
||||
const claudeModels = Object.keys(data.anthropic.models).filter((m) => m.includes("claude"));
|
||||
assert.ok(claudeModels.length > 0, "should have claude models");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user