mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
Merge release/v3.8.8 into refactor/pages-v3-14 (CLI pages redesign)
Conflicts: CLAUDE.md base; i18n deep-merge (costsSection=Custos); .source regenerated (fumadocs-mdx, +1 doc); openapi regenerated. CLIToolsPageClient.tsx: accepted #2839 deletion (redesign replaced cli-tools/ with cli-code/cli-agents/acp-agents; base #2858 only removed obsolete MITM cards; AgentBridge reachable via sidebar; 0 orphan refs). sidebar-visibility test passes (cli items + agent-bridge merged).
This commit is contained in:
@@ -7,10 +7,18 @@ import Button from "./Button";
|
||||
import Input from "./Input";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
|
||||
const GOOGLE_OAUTH_PROVIDERS = new Set(["antigravity", "gemini-cli"]);
|
||||
const GOOGLE_OAUTH_PROVIDERS = new Set(["antigravity", "agy", "gemini-cli"]);
|
||||
|
||||
/** Providers that use a local callback server on a random port (PKCE browser flow). */
|
||||
const PKCE_CALLBACK_SERVER_PROVIDERS = new Set(["codex", "windsurf", "devin-cli"]);
|
||||
const PKCE_CALLBACK_SERVER_PROVIDERS = new Set(["codex"]);
|
||||
|
||||
/**
|
||||
* Phase 1 hotfix (2026-05-29): windsurf & devin-cli only support import-token.
|
||||
* Their PKCE flow targeting app.devin.ai/editor/signin returned 404 post-rebrand.
|
||||
* Phase 2 will reintroduce browser login via Firebase OAuth + RegisterUser.
|
||||
* Spec: docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md.
|
||||
*/
|
||||
const IMPORT_TOKEN_ONLY_PROVIDERS = new Set(["windsurf", "devin-cli"]);
|
||||
|
||||
type OAuthModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -45,11 +53,16 @@ export default function OAuthModal({
|
||||
const [deviceData, setDeviceData] = useState(null);
|
||||
const [polling, setPolling] = useState(false);
|
||||
// API-key paste mode: for providers that accept a token directly (windsurf, devin-cli)
|
||||
const [showPasteToken, setShowPasteToken] = useState(false);
|
||||
const [showPasteToken, setShowPasteToken] = useState(
|
||||
provider === "windsurf" || provider === "devin-cli"
|
||||
);
|
||||
const [pasteToken, setPasteToken] = useState("");
|
||||
const [savingToken, setSavingToken] = useState(false);
|
||||
|
||||
const supportsTokenPaste = provider === "windsurf" || provider === "devin-cli";
|
||||
// Phase 1 hotfix (2026-05-29): windsurf/devin-cli are import-token-only.
|
||||
// Hide the "Browser Login" tab — Phase 2 will restore it via Firebase OAuth.
|
||||
const importTokenOnly = IMPORT_TOKEN_ONLY_PROVIDERS.has(provider);
|
||||
const popupRef = useRef(null);
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
const deviceVerificationUrl =
|
||||
@@ -695,8 +708,10 @@ export default function OAuthModal({
|
||||
size="lg"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Paste-token tab toggle (Windsurf / Devin CLI only) */}
|
||||
{supportsTokenPaste && step !== "success" && (
|
||||
{/* Paste-token tab toggle (Windsurf / Devin CLI only).
|
||||
Phase 1 hotfix: when importTokenOnly is true, hide the entire toggle —
|
||||
there is no "Browser Login" tab to switch to until Phase 2 ships. */}
|
||||
{supportsTokenPaste && !importTokenOnly && step !== "success" && (
|
||||
<div className="flex gap-2 border-b border-border pb-3">
|
||||
<button
|
||||
className={`text-sm px-3 py-1 rounded-t ${!showPasteToken ? "font-semibold border-b-2 border-primary text-primary" : "text-text-muted"}`}
|
||||
|
||||
@@ -94,10 +94,10 @@ export default function ProxyLogDetail({ log, onClose }) {
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mb-1">
|
||||
Public IP
|
||||
Client IP
|
||||
</div>
|
||||
<div className="text-sm font-medium font-mono text-emerald-400">
|
||||
{log.publicIp || "—"}
|
||||
{log.clientIp || "—"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function ProxyLogger() {
|
||||
{ key: "provider", label: t("colProvider") },
|
||||
{ key: "target", label: t("colTarget") },
|
||||
{ key: "latency", label: t("colLatency") },
|
||||
{ key: "ip", label: t("colPublicIp") },
|
||||
{ key: "ip", label: t("colClientIp") },
|
||||
{ key: "time", label: t("colTime") },
|
||||
],
|
||||
[t]
|
||||
@@ -435,7 +435,7 @@ export default function ProxyLogger() {
|
||||
)}
|
||||
{visibleColumns.ip && (
|
||||
<th className="px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px]">
|
||||
{t("colPublicIp")}
|
||||
{t("colClientIp")}
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.time && (
|
||||
@@ -552,7 +552,7 @@ export default function ProxyLogger() {
|
||||
)}
|
||||
{visibleColumns.ip && (
|
||||
<td className="px-3 py-2 font-mono text-[11px] text-emerald-400">
|
||||
{log.publicIp || "—"}
|
||||
{log.clientIp || "—"}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.time && (
|
||||
|
||||
81
src/shared/components/RiskNoticeModal.tsx
Normal file
81
src/shared/components/RiskNoticeModal.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Button from "@/shared/components/Button";
|
||||
|
||||
export interface RiskNoticeModalProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
body: string;
|
||||
dontShowAgainKey: string;
|
||||
onAccept: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic risk notice modal (D16).
|
||||
* Persists "don't show again" preference to localStorage using `dontShowAgainKey`.
|
||||
*/
|
||||
export function RiskNoticeModal({
|
||||
open,
|
||||
title,
|
||||
body,
|
||||
dontShowAgainKey,
|
||||
onAccept,
|
||||
onCancel,
|
||||
}: RiskNoticeModalProps) {
|
||||
const t = useTranslations("common");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onCancel();
|
||||
};
|
||||
document.addEventListener("keydown", handler);
|
||||
return () => document.removeEventListener("keydown", handler);
|
||||
}, [open, onCancel]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleAccept = () => {
|
||||
try {
|
||||
localStorage.setItem(dontShowAgainKey, "true");
|
||||
} catch {
|
||||
// ignore storage errors
|
||||
}
|
||||
onAccept();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="risk-modal-title"
|
||||
>
|
||||
<div className="w-full max-w-md rounded-xl border border-amber-500/30 bg-card p-6 shadow-xl">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">warning</span>
|
||||
</div>
|
||||
<h2 id="risk-modal-title" className="text-base font-semibold text-text-main pt-1">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-muted mb-6 leading-relaxed">{body}</p>
|
||||
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
{t("cancel") || "Cancel"}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleAccept}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">check</span>
|
||||
{t("understand") || "I understand"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -301,6 +301,7 @@ const LOBE_PROVIDER_ALIASES = {
|
||||
"amazon-q": "Aws",
|
||||
anthropic: "Anthropic",
|
||||
antigravity: "Antigravity",
|
||||
agy: "Antigravity", // Antigravity CLI — same brand icon as the antigravity provider
|
||||
assemblyai: "AssemblyAI",
|
||||
"aws-polly": "Aws",
|
||||
azure: "Azure",
|
||||
|
||||
@@ -187,6 +187,20 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-4-7", "claude-opus-4.7"),
|
||||
},
|
||||
|
||||
// ── Claude Opus 4.8 ─────────────────────────────────────────────
|
||||
"claude-opus-4-8": {
|
||||
maxOutputTokens: 128000,
|
||||
contextWindow: 1000000,
|
||||
// Opus 4.8 inherits Opus 4.7's adaptive thinking constraints: no fixed
|
||||
// thinking budget requests, with effort controlled by output_config.
|
||||
defaultThinkingBudget: 32000,
|
||||
thinkingBudgetCap: 120000,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-4-8", "claude-opus-4.8"),
|
||||
},
|
||||
|
||||
// ── Claude Sonnet 4.5 ───────────────────────────────────────────
|
||||
"claude-sonnet-4-5-20250929": {
|
||||
maxOutputTokens: 64000,
|
||||
|
||||
@@ -129,6 +129,13 @@ export const DEFAULT_PRICING = {
|
||||
|
||||
// Claude Code (cc)
|
||||
cc: {
|
||||
"claude-opus-4-8": {
|
||||
input: 5.0,
|
||||
output: 25.0,
|
||||
cached: 2.5,
|
||||
reasoning: 25.0,
|
||||
cache_creation: 5.0,
|
||||
},
|
||||
"claude-opus-4-7": {
|
||||
input: 5.0,
|
||||
output: 25.0,
|
||||
@@ -710,6 +717,9 @@ export const DEFAULT_PRICING = {
|
||||
// Common model IDs (without dates) used across providers
|
||||
// Intentional duplicates of dot-notation variants (e.g. claude-opus-4.6)
|
||||
// to cover hyphen-notation IDs (claude-opus-4-6) used by some clients
|
||||
"claude-opus-4.8": CLAUDE_OPUS_4_PRICING,
|
||||
"claude-opus-4-8": CLAUDE_OPUS_4_PRICING,
|
||||
"claude-opus-4-7": CLAUDE_OPUS_4_PRICING,
|
||||
"claude-opus-4-6": CLAUDE_OPUS_46_PRICING,
|
||||
"claude-sonnet-4-6": CLAUDE_SONNET_46_PRICING,
|
||||
"claude-opus-4-5-20251101": CLAUDE_OPUS_4_PRICING,
|
||||
|
||||
@@ -89,6 +89,20 @@ export const OAUTH_PROVIDERS = {
|
||||
authHint:
|
||||
"Uses Gemini CLI OAuth / Cloud Code credentials. Pro models require an eligible Google account or paid plan.",
|
||||
},
|
||||
agy: {
|
||||
id: "agy",
|
||||
alias: "agy",
|
||||
name: "Antigravity CLI",
|
||||
icon: "terminal",
|
||||
color: "#F59E0B",
|
||||
textIcon: "AGY",
|
||||
website: "https://antigravity.google",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "oauth",
|
||||
hasFree: true,
|
||||
authHint:
|
||||
"Import your Antigravity CLI (`agy`) login (paste/upload its token file), auto-detect a local CLI login, or sign in with Google. Shares the Antigravity backend (incl. Claude models).",
|
||||
},
|
||||
kiro: {
|
||||
id: "kiro",
|
||||
alias: "kr",
|
||||
@@ -309,6 +323,8 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
color: "#0866FF",
|
||||
textIcon: "MS",
|
||||
website: "https://www.meta.ai",
|
||||
hasFree: true,
|
||||
freeNote: "Free with login — Meta AI platform with Llama models.",
|
||||
authHint: "Paste your abra_sess value or full cookie header from meta.ai",
|
||||
},
|
||||
"claude-web": {
|
||||
@@ -384,6 +400,8 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
color: "#1A56DB",
|
||||
textIcon: "IA",
|
||||
website: "https://app.innerai.com",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
authHint:
|
||||
"Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com",
|
||||
},
|
||||
@@ -395,9 +413,109 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
color: "#6E3AD3",
|
||||
textIcon: "AW",
|
||||
website: "https://agent.adapta.one",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
authHint:
|
||||
"Paste your __client cookie value from .clerk.agent.adapta.one (DevTools → Application → Cookies)",
|
||||
},
|
||||
"duckduckgo-web": {
|
||||
id: "duckduckgo-web",
|
||||
alias: "ddgw",
|
||||
name: "DuckDuckGo AI Chat",
|
||||
icon: "auto_awesome",
|
||||
color: "#DE5833",
|
||||
textIcon: "DDG",
|
||||
website: "https://duckduckgo.com/duckchat",
|
||||
hasFree: true,
|
||||
freeNote: "Free — anonymous access to multiple AI models via DuckDuckGo.",
|
||||
authHint: "No credentials required — DuckDuckGo AI Chat is anonymous and free.",
|
||||
},
|
||||
huggingchat: {
|
||||
id: "huggingchat",
|
||||
alias: "hc",
|
||||
name: "HuggingChat (Free)",
|
||||
icon: "auto_awesome",
|
||||
color: "#FFD21E",
|
||||
textIcon: "HC",
|
||||
website: "https://huggingface.co/chat",
|
||||
hasFree: true,
|
||||
freeNote: "Free LLM chat — no subscription required. Rate limits apply.",
|
||||
authHint:
|
||||
"Paste your hf-chat cookie value from huggingface.co/chat (DevTools → Application → Cookies → hf-chat). Optional — works without auth for basic use.",
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
phind: {
|
||||
id: "phind",
|
||||
alias: "ph",
|
||||
name: "Phind (Free)",
|
||||
icon: "auto_awesome",
|
||||
color: "#000000",
|
||||
textIcon: "PH",
|
||||
website: "https://www.phind.com",
|
||||
hasFree: true,
|
||||
freeNote: "Free dev-focused AI chat with code search. Rate limits apply.",
|
||||
authHint:
|
||||
"Paste your session cookie from phind.com (DevTools → Application → Cookies). Optional — works with free tier.",
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"poe-web": {
|
||||
id: "poe-web",
|
||||
alias: "poe",
|
||||
name: "Poe Web (Subscription)",
|
||||
icon: "auto_awesome",
|
||||
color: "#6C3AED",
|
||||
textIcon: "PW",
|
||||
website: "https://poe.com",
|
||||
authHint: "Paste your p-b cookie value from poe.com (DevTools → Application → Cookies → p-b)",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"venice-web": {
|
||||
id: "venice-web",
|
||||
alias: "ven",
|
||||
name: "Venice Web (Privacy)",
|
||||
icon: "auto_awesome",
|
||||
color: "#22C55E",
|
||||
textIcon: "VW",
|
||||
website: "https://venice.ai",
|
||||
authHint: "Paste your session cookie from venice.ai (DevTools → Application → Cookies)",
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"v0-vercel-web": {
|
||||
id: "v0-vercel-web",
|
||||
alias: "v0",
|
||||
name: "v0 Vercel Web (Code Gen)",
|
||||
icon: "auto_awesome",
|
||||
color: "#000000",
|
||||
textIcon: "V0",
|
||||
website: "https://v0.dev",
|
||||
authHint: "Paste your session cookie from v0.dev (DevTools → Application → Cookies)",
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"kimi-web": {
|
||||
id: "kimi-web",
|
||||
alias: "kimi",
|
||||
name: "Kimi Web (Moonshot AI)",
|
||||
icon: "auto_awesome",
|
||||
color: "#2563EB",
|
||||
textIcon: "KW",
|
||||
website: "https://kimi.moonshot.cn",
|
||||
authHint: "Paste your session cookie from kimi.moonshot.cn (DevTools → Application → Cookies)",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"doubao-web": {
|
||||
id: "doubao-web",
|
||||
alias: "db",
|
||||
name: "Doubao Web (ByteDance)",
|
||||
icon: "auto_awesome",
|
||||
color: "#3B82F6",
|
||||
textIcon: "DW",
|
||||
website: "https://www.doubao.com",
|
||||
authHint: "Paste your session cookie from doubao.com (DevTools → Application → Cookies)",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
};
|
||||
|
||||
// API Key Providers
|
||||
@@ -2816,22 +2934,114 @@ export const SYSTEM_PROVIDERS = {
|
||||
},
|
||||
};
|
||||
|
||||
// All providers (combined)
|
||||
export const AI_PROVIDERS = {
|
||||
...NOAUTH_PROVIDERS,
|
||||
...OAUTH_PROVIDERS,
|
||||
...APIKEY_PROVIDERS,
|
||||
...WEB_COOKIE_PROVIDERS,
|
||||
...LOCAL_PROVIDERS,
|
||||
...SEARCH_PROVIDERS,
|
||||
...AUDIO_ONLY_PROVIDERS,
|
||||
...UPSTREAM_PROXY_PROVIDERS,
|
||||
...CLOUD_AGENT_PROVIDERS,
|
||||
...SYSTEM_PROVIDERS, // <-- system providers included
|
||||
};
|
||||
const _PROVIDER_SECTIONS = [
|
||||
NOAUTH_PROVIDERS,
|
||||
OAUTH_PROVIDERS,
|
||||
APIKEY_PROVIDERS,
|
||||
WEB_COOKIE_PROVIDERS,
|
||||
LOCAL_PROVIDERS,
|
||||
SEARCH_PROVIDERS,
|
||||
AUDIO_ONLY_PROVIDERS,
|
||||
UPSTREAM_PROXY_PROVIDERS,
|
||||
CLOUD_AGENT_PROVIDERS,
|
||||
SYSTEM_PROVIDERS,
|
||||
] as const;
|
||||
|
||||
export type AiProviderId = keyof typeof AI_PROVIDERS;
|
||||
export type AiProviderDefinition = (typeof AI_PROVIDERS)[AiProviderId];
|
||||
let _aiProviders: Record<string, any> | null = null;
|
||||
|
||||
function getOrCreateAiProviders(): Record<string, any> {
|
||||
if (!_aiProviders) {
|
||||
_aiProviders = {};
|
||||
for (const section of _PROVIDER_SECTIONS) {
|
||||
Object.assign(_aiProviders, section);
|
||||
}
|
||||
}
|
||||
return _aiProviders;
|
||||
}
|
||||
|
||||
let _ALIAS_TO_ID: Record<string, string> | null = null;
|
||||
|
||||
function getOrCreateAliasToId(): Record<string, string> {
|
||||
if (!_ALIAS_TO_ID) {
|
||||
_ALIAS_TO_ID = {};
|
||||
for (const section of _PROVIDER_SECTIONS) {
|
||||
for (const p of Object.values(section)) {
|
||||
if ((p as any).alias) _ALIAS_TO_ID[(p as any).alias] = (p as any).id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _ALIAS_TO_ID;
|
||||
}
|
||||
|
||||
let _ID_TO_ALIAS: Record<string, string> | null = null;
|
||||
|
||||
function getOrCreateIdToAlias(): Record<string, string> {
|
||||
if (!_ID_TO_ALIAS) {
|
||||
_ID_TO_ALIAS = {};
|
||||
for (const section of _PROVIDER_SECTIONS) {
|
||||
for (const p of Object.values(section)) {
|
||||
_ID_TO_ALIAS[(p as any).id] = (p as any).alias || (p as any).id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _ID_TO_ALIAS;
|
||||
}
|
||||
|
||||
export function getProviderById(id: string) {
|
||||
return (NOAUTH_PROVIDERS as Record<string, any>)[id]
|
||||
?? (OAUTH_PROVIDERS as Record<string, any>)[id]
|
||||
?? (APIKEY_PROVIDERS as Record<string, any>)[id]
|
||||
?? (WEB_COOKIE_PROVIDERS as Record<string, any>)[id]
|
||||
?? (LOCAL_PROVIDERS as Record<string, any>)[id]
|
||||
?? (SEARCH_PROVIDERS as Record<string, any>)[id]
|
||||
?? (AUDIO_ONLY_PROVIDERS as Record<string, any>)[id]
|
||||
?? (UPSTREAM_PROXY_PROVIDERS as Record<string, any>)[id]
|
||||
?? (CLOUD_AGENT_PROVIDERS as Record<string, any>)[id]
|
||||
?? (SYSTEM_PROVIDERS as Record<string, any>)[id]
|
||||
?? undefined;
|
||||
}
|
||||
|
||||
export const AI_PROVIDERS = new Proxy({} as Record<string, any>, {
|
||||
get(_, key) {
|
||||
if (key === "then") return undefined;
|
||||
return typeof key === "string" ? getOrCreateAiProviders()[key] : undefined;
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(getOrCreateAiProviders());
|
||||
},
|
||||
has(_, key) {
|
||||
return key in getOrCreateAiProviders();
|
||||
},
|
||||
getOwnPropertyDescriptor(_, key) {
|
||||
const obj = getOrCreateAiProviders();
|
||||
if (typeof key === "string" && key in obj) {
|
||||
return { configurable: true, enumerable: true, value: obj[key] };
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export type AiProviderId = keyof typeof NOAUTH_PROVIDERS
|
||||
| keyof typeof OAUTH_PROVIDERS
|
||||
| keyof typeof APIKEY_PROVIDERS
|
||||
| keyof typeof WEB_COOKIE_PROVIDERS
|
||||
| keyof typeof LOCAL_PROVIDERS
|
||||
| keyof typeof SEARCH_PROVIDERS
|
||||
| keyof typeof AUDIO_ONLY_PROVIDERS
|
||||
| keyof typeof UPSTREAM_PROXY_PROVIDERS
|
||||
| keyof typeof CLOUD_AGENT_PROVIDERS
|
||||
| keyof typeof SYSTEM_PROVIDERS;
|
||||
|
||||
export type AiProviderDefinition = (typeof NOAUTH_PROVIDERS)[keyof typeof NOAUTH_PROVIDERS]
|
||||
| (typeof OAUTH_PROVIDERS)[keyof typeof OAUTH_PROVIDERS]
|
||||
| (typeof APIKEY_PROVIDERS)[keyof typeof APIKEY_PROVIDERS]
|
||||
| (typeof WEB_COOKIE_PROVIDERS)[keyof typeof WEB_COOKIE_PROVIDERS]
|
||||
| (typeof LOCAL_PROVIDERS)[keyof typeof LOCAL_PROVIDERS]
|
||||
| (typeof SEARCH_PROVIDERS)[keyof typeof SEARCH_PROVIDERS]
|
||||
| (typeof AUDIO_ONLY_PROVIDERS)[keyof typeof AUDIO_ONLY_PROVIDERS]
|
||||
| (typeof UPSTREAM_PROXY_PROVIDERS)[keyof typeof UPSTREAM_PROXY_PROVIDERS]
|
||||
| (typeof CLOUD_AGENT_PROVIDERS)[keyof typeof CLOUD_AGENT_PROVIDERS]
|
||||
| (typeof SYSTEM_PROVIDERS)[keyof typeof SYSTEM_PROVIDERS];
|
||||
|
||||
// Auth methods
|
||||
export const AUTH_METHODS = {
|
||||
@@ -2839,11 +3049,12 @@ export const AUTH_METHODS = {
|
||||
apikey: { id: "apikey", name: "API Key", icon: "key" },
|
||||
};
|
||||
|
||||
// Helper: Get provider by alias
|
||||
export function getProviderByAlias(alias: string): AiProviderDefinition | null {
|
||||
for (const provider of Object.values(AI_PROVIDERS)) {
|
||||
if (provider.alias === alias || provider.id === alias) {
|
||||
return provider;
|
||||
for (const section of _PROVIDER_SECTIONS) {
|
||||
for (const provider of Object.values(section)) {
|
||||
if (provider.alias === alias || provider.id === alias) {
|
||||
return provider as AiProviderDefinition;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -2855,29 +3066,53 @@ export function resolveProviderId(aliasOrId: string): string {
|
||||
return provider?.id || aliasOrId;
|
||||
}
|
||||
|
||||
// Helper: Get alias from provider ID
|
||||
export function getProviderAlias(providerId: string): string {
|
||||
const provider = Object.prototype.hasOwnProperty.call(AI_PROVIDERS, providerId)
|
||||
? AI_PROVIDERS[providerId as AiProviderId]
|
||||
: undefined;
|
||||
const provider = getProviderById(providerId);
|
||||
return provider?.alias || providerId;
|
||||
}
|
||||
|
||||
// Alias to ID mapping (for quick lookup)
|
||||
export const ALIAS_TO_ID = Object.values(AI_PROVIDERS).reduce<Record<string, string>>((acc, p) => {
|
||||
if (p.alias) acc[p.alias] = p.id;
|
||||
return acc;
|
||||
}, {});
|
||||
export const ALIAS_TO_ID = new Proxy({} as Record<string, string>, {
|
||||
get(_, key) {
|
||||
return typeof key === "string" ? getOrCreateAliasToId()[key] : undefined;
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(getOrCreateAliasToId());
|
||||
},
|
||||
has(_, key) {
|
||||
return key in getOrCreateAliasToId();
|
||||
},
|
||||
getOwnPropertyDescriptor(_, key) {
|
||||
const obj = getOrCreateAliasToId();
|
||||
if (typeof key === "string" && key in obj) {
|
||||
return { configurable: true, enumerable: true, value: obj[key] };
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
// ID to Alias mapping
|
||||
export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce<Record<string, string>>((acc, p) => {
|
||||
acc[p.id] = p.alias || p.id;
|
||||
return acc;
|
||||
}, {});
|
||||
export const ID_TO_ALIAS = new Proxy({} as Record<string, string>, {
|
||||
get(_, key) {
|
||||
return typeof key === "string" ? getOrCreateIdToAlias()[key] : undefined;
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(getOrCreateIdToAlias());
|
||||
},
|
||||
has(_, key) {
|
||||
return key in getOrCreateIdToAlias();
|
||||
},
|
||||
getOwnPropertyDescriptor(_, key) {
|
||||
const obj = getOrCreateIdToAlias();
|
||||
if (typeof key === "string" && key in obj) {
|
||||
return { configurable: true, enumerable: true, value: obj[key] };
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
// Providers that support usage/quota API
|
||||
export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
"antigravity",
|
||||
"agy",
|
||||
"gemini-cli",
|
||||
"kiro",
|
||||
"amazon-q",
|
||||
@@ -2890,6 +3125,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
"glm-cn",
|
||||
"zai",
|
||||
"glmt",
|
||||
"opencode-go",
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
"crof",
|
||||
|
||||
20
src/shared/constants/selfServiceScopes.ts
Normal file
20
src/shared/constants/selfServiceScopes.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const SELF_USAGE_SCOPE = "self:usage";
|
||||
export const SELF_ACCOUNT_QUOTA_SCOPE = "self:account-quota";
|
||||
|
||||
export const DEFAULT_SELF_SERVICE_SCOPES = [SELF_USAGE_SCOPE] as const;
|
||||
|
||||
export function hasSelfUsageScope(scopes: readonly string[] | null | undefined): boolean {
|
||||
return Array.isArray(scopes) && scopes.includes(SELF_USAGE_SCOPE);
|
||||
}
|
||||
|
||||
export function hasSelfAccountQuotaScope(scopes: readonly string[] | null | undefined): boolean {
|
||||
return Array.isArray(scopes) && scopes.includes(SELF_ACCOUNT_QUOTA_SCOPE);
|
||||
}
|
||||
|
||||
export function normalizeSelfServiceScopesForCreate(
|
||||
scopes: readonly string[] | null | undefined
|
||||
): string[] {
|
||||
const normalized = new Set((scopes ?? []).filter((scope) => typeof scope === "string" && scope));
|
||||
normalized.add(SELF_USAGE_SCOPE);
|
||||
return [...normalized];
|
||||
}
|
||||
@@ -17,6 +17,8 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
|
||||
"cli-agents",
|
||||
"acp-agents",
|
||||
"cloud-agents",
|
||||
"agent-bridge",
|
||||
"traffic-inspector",
|
||||
// OmniProxy > Integrations
|
||||
"api-endpoints",
|
||||
"webhooks",
|
||||
@@ -34,16 +36,18 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
|
||||
"analytics-search",
|
||||
"analytics-evals",
|
||||
// Monitoring — flat
|
||||
"activity",
|
||||
"logs",
|
||||
"logs-proxy",
|
||||
"logs-console",
|
||||
"logs-activity",
|
||||
"health",
|
||||
"runtime",
|
||||
// Monitoring > Costs Parameters
|
||||
// Costs section
|
||||
"costs-pricing",
|
||||
"costs-budget",
|
||||
"costs-quota-share",
|
||||
"costs-quota-plans",
|
||||
// Monitoring > Audit
|
||||
"audit",
|
||||
"audit-mcp",
|
||||
@@ -90,6 +94,7 @@ export type SidebarSectionId =
|
||||
| "home"
|
||||
| "omni-proxy"
|
||||
| "analytics"
|
||||
| "costs"
|
||||
| "monitoring"
|
||||
| "devtools"
|
||||
| "agentic-features"
|
||||
@@ -257,6 +262,20 @@ const TOOLS_GROUP: SidebarItemGroup = {
|
||||
subtitleKey: "cloudAgentsSubtitle",
|
||||
icon: "cloud",
|
||||
},
|
||||
{
|
||||
id: "agent-bridge",
|
||||
href: "/dashboard/tools/agent-bridge",
|
||||
i18nKey: "agentBridge",
|
||||
subtitleKey: "agentBridgeSubtitle",
|
||||
icon: "link",
|
||||
},
|
||||
{
|
||||
id: "traffic-inspector",
|
||||
href: "/dashboard/tools/traffic-inspector",
|
||||
i18nKey: "trafficInspector",
|
||||
subtitleKey: "trafficInspectorSubtitle",
|
||||
icon: "network_check",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -321,13 +340,6 @@ const ANALYTICS_ITEMS: readonly SidebarItemDefinition[] = [
|
||||
subtitleKey: "analyticsUtilizationSubtitle",
|
||||
icon: "bar_chart",
|
||||
},
|
||||
{
|
||||
id: "costs",
|
||||
href: "/dashboard/costs",
|
||||
i18nKey: "costs",
|
||||
subtitleKey: "costsSubtitle",
|
||||
icon: "account_balance_wallet",
|
||||
},
|
||||
{
|
||||
id: "cache",
|
||||
href: "/dashboard/cache",
|
||||
@@ -360,79 +372,105 @@ const ANALYTICS_ITEMS: readonly SidebarItemDefinition[] = [
|
||||
|
||||
const MONITORING_ITEMS: readonly SidebarItemDefinition[] = [
|
||||
{
|
||||
id: "logs",
|
||||
href: "/dashboard/logs",
|
||||
i18nKey: "logs",
|
||||
subtitleKey: "logsSubtitle",
|
||||
icon: "description",
|
||||
},
|
||||
{
|
||||
id: "logs-proxy",
|
||||
href: "/dashboard/logs/proxy",
|
||||
i18nKey: "logsProxy",
|
||||
subtitleKey: "logsProxySubtitle",
|
||||
icon: "lan",
|
||||
},
|
||||
{
|
||||
id: "logs-console",
|
||||
href: "/dashboard/logs/console",
|
||||
i18nKey: "consoleLogs",
|
||||
subtitleKey: "consoleLogsSubtitle",
|
||||
icon: "terminal",
|
||||
},
|
||||
{
|
||||
id: "logs-activity",
|
||||
href: "/dashboard/logs/activity",
|
||||
i18nKey: "logsActivity",
|
||||
subtitleKey: "logsActivitySubtitle",
|
||||
icon: "history",
|
||||
},
|
||||
{
|
||||
id: "health",
|
||||
href: "/dashboard/health",
|
||||
i18nKey: "health",
|
||||
subtitleKey: "healthSubtitle",
|
||||
icon: "health_and_safety",
|
||||
},
|
||||
{
|
||||
id: "runtime",
|
||||
href: "/dashboard/runtime",
|
||||
i18nKey: "runtime",
|
||||
subtitleKey: "runtimeSubtitle",
|
||||
icon: "bolt",
|
||||
id: "activity",
|
||||
href: "/dashboard/activity",
|
||||
i18nKey: "activity",
|
||||
subtitleKey: "activitySubtitle",
|
||||
icon: "timeline",
|
||||
},
|
||||
];
|
||||
|
||||
const COSTS_PARAMS_GROUP: SidebarItemGroup = {
|
||||
const LOGS_GROUP: SidebarItemGroup = {
|
||||
type: "group",
|
||||
id: "costs-parameters",
|
||||
titleKey: "costsParametersGroup",
|
||||
titleFallback: "Costs Parameters",
|
||||
id: "logs",
|
||||
titleKey: "logsGroup",
|
||||
titleFallback: "Logs",
|
||||
items: [
|
||||
{
|
||||
id: "costs-pricing",
|
||||
href: "/dashboard/costs/pricing",
|
||||
i18nKey: "costsPricing",
|
||||
subtitleKey: "costsPricingSubtitle",
|
||||
icon: "price_change",
|
||||
id: "logs",
|
||||
href: "/dashboard/logs",
|
||||
i18nKey: "logs",
|
||||
subtitleKey: "logsSubtitle",
|
||||
icon: "description",
|
||||
},
|
||||
{
|
||||
id: "costs-budget",
|
||||
href: "/dashboard/costs/budget",
|
||||
i18nKey: "costsBudget",
|
||||
subtitleKey: "costsBudgetSubtitle",
|
||||
icon: "savings",
|
||||
id: "logs-proxy",
|
||||
href: "/dashboard/logs/proxy",
|
||||
i18nKey: "logsProxy",
|
||||
subtitleKey: "logsProxySubtitle",
|
||||
icon: "lan",
|
||||
},
|
||||
{
|
||||
id: "costs-quota-share",
|
||||
href: "/dashboard/costs/quota-share",
|
||||
i18nKey: "costsQuotaShare",
|
||||
subtitleKey: "costsQuotaShareSubtitle",
|
||||
icon: "pie_chart",
|
||||
id: "logs-console",
|
||||
href: "/dashboard/logs/console",
|
||||
i18nKey: "consoleLogs",
|
||||
subtitleKey: "consoleLogsSubtitle",
|
||||
icon: "terminal",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const SYSTEM_GROUP: SidebarItemGroup = {
|
||||
type: "group",
|
||||
id: "system",
|
||||
titleKey: "systemGroup",
|
||||
titleFallback: "System",
|
||||
items: [
|
||||
{
|
||||
id: "health",
|
||||
href: "/dashboard/health",
|
||||
i18nKey: "health",
|
||||
subtitleKey: "healthSubtitle",
|
||||
icon: "health_and_safety",
|
||||
},
|
||||
{
|
||||
id: "runtime",
|
||||
href: "/dashboard/runtime",
|
||||
i18nKey: "runtime",
|
||||
subtitleKey: "runtimeSubtitle",
|
||||
icon: "bolt",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const COSTS_ITEMS: readonly SidebarItemDefinition[] = [
|
||||
{
|
||||
id: "costs",
|
||||
href: "/dashboard/costs",
|
||||
i18nKey: "costsOverview",
|
||||
subtitleKey: "costsOverviewSubtitle",
|
||||
icon: "account_balance_wallet",
|
||||
},
|
||||
{
|
||||
id: "costs-pricing",
|
||||
href: "/dashboard/costs/pricing",
|
||||
i18nKey: "costsPricing",
|
||||
subtitleKey: "costsPricingSubtitle",
|
||||
icon: "price_change",
|
||||
},
|
||||
{
|
||||
id: "costs-budget",
|
||||
href: "/dashboard/costs/budget",
|
||||
i18nKey: "costsBudget",
|
||||
subtitleKey: "costsBudgetSubtitle",
|
||||
icon: "savings",
|
||||
},
|
||||
{
|
||||
id: "costs-quota-share",
|
||||
href: "/dashboard/costs/quota-share",
|
||||
i18nKey: "costsQuotaShare",
|
||||
subtitleKey: "costsQuotaShareSubtitle",
|
||||
icon: "pie_chart",
|
||||
},
|
||||
{
|
||||
id: "costs-quota-plans",
|
||||
href: "/dashboard/costs/quota-share/plans",
|
||||
i18nKey: "costsQuotaPlans",
|
||||
subtitleKey: "costsQuotaPlansSubtitle",
|
||||
icon: "fact_check",
|
||||
},
|
||||
];
|
||||
|
||||
const AUDIT_GROUP: SidebarItemGroup = {
|
||||
type: "group",
|
||||
id: "audit",
|
||||
@@ -726,11 +764,17 @@ export const SIDEBAR_SECTIONS: readonly SidebarSectionDefinition[] = [
|
||||
titleFallback: "Analytics",
|
||||
children: ANALYTICS_ITEMS,
|
||||
},
|
||||
{
|
||||
id: "costs",
|
||||
titleKey: "costsSection",
|
||||
titleFallback: "Costs",
|
||||
children: COSTS_ITEMS,
|
||||
},
|
||||
{
|
||||
id: "monitoring",
|
||||
titleKey: "monitoringSection",
|
||||
titleFallback: "Monitoring",
|
||||
children: [...MONITORING_ITEMS, COSTS_PARAMS_GROUP, AUDIT_GROUP],
|
||||
children: [...MONITORING_ITEMS, LOGS_GROUP, AUDIT_GROUP, SYSTEM_GROUP],
|
||||
},
|
||||
{
|
||||
id: "devtools",
|
||||
@@ -851,7 +895,7 @@ const ADMIN_SHOWN: ReadonlySet<HideableSidebarItemId> = new Set([
|
||||
"costs-quota-share",
|
||||
"cache",
|
||||
"logs",
|
||||
"logs-activity",
|
||||
"activity",
|
||||
"health",
|
||||
"runtime",
|
||||
"audit",
|
||||
|
||||
37
src/shared/schemas/agentBridge.ts
Normal file
37
src/shared/schemas/agentBridge.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const AgentBridgeStateRowSchema = z.object({
|
||||
agent_id: z.string(),
|
||||
dns_enabled: z.boolean(),
|
||||
cert_trusted: z.boolean(),
|
||||
setup_completed: z.boolean(),
|
||||
last_started_at: z.string().datetime().nullable(),
|
||||
last_error: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const AgentBridgeMappingRowSchema = z.object({
|
||||
agent_id: z.string(),
|
||||
source_model: z.string(),
|
||||
target_model: z.string(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const AgentBridgeBypassRowSchema = z.object({
|
||||
pattern: z.string(),
|
||||
source: z.enum(["default", "user"]),
|
||||
created_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const AgentBridgeServerActionSchema = z.object({
|
||||
action: z.enum(["start", "stop", "restart", "trust-cert", "regenerate-cert"]),
|
||||
});
|
||||
|
||||
export const AgentBridgeDnsActionSchema = z.object({ enabled: z.boolean() });
|
||||
|
||||
export const AgentBridgeMappingPutSchema = z.object({
|
||||
mappings: z.array(z.object({ source: z.string(), target: z.string() })),
|
||||
});
|
||||
|
||||
export const AgentBridgeBypassUpsertSchema = z.object({ patterns: z.array(z.string()) });
|
||||
|
||||
export const AgentBridgeUpstreamCaPostSchema = z.object({ path: z.string().min(1) });
|
||||
47
src/shared/schemas/inspector.ts
Normal file
47
src/shared/schemas/inspector.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const InspectorCustomHostSchema = z.object({
|
||||
host: z.string().min(1),
|
||||
enabled: z.boolean().default(true),
|
||||
label: z.string().nullable().optional(),
|
||||
kind: z.enum(["llm", "app", "custom"]).default("custom"),
|
||||
});
|
||||
|
||||
export const InspectorSessionStartSchema = z.object({ name: z.string().optional() });
|
||||
|
||||
export const InspectorSessionPatchSchema = z.object({
|
||||
action: z.enum(["stop", "rename"]),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
export const InspectorCaptureModeActionSchema = z.object({
|
||||
action: z.enum(["start", "stop"]),
|
||||
});
|
||||
|
||||
export const InspectorSystemProxyActionSchema = z.object({
|
||||
action: z.enum(["apply", "revert"]),
|
||||
port: z.number().int().positive().max(65535).optional(),
|
||||
guardMinutes: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
export const InspectorTlsInterceptToggleSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const InspectorAnnotationPutSchema = z.object({
|
||||
annotation: z.string().max(10_000),
|
||||
});
|
||||
|
||||
// 1 MB cap — matches INSPECTOR_MAX_BODY_KB constant
|
||||
export const InspectorSessionRequestAppendSchema = z.object({
|
||||
payload: z.string().max(1_048_576),
|
||||
});
|
||||
|
||||
export const InspectorListQuerySchema = z.object({
|
||||
profile: z.enum(["llm", "custom", "all"]).optional(),
|
||||
host: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
status: z.enum(["2xx", "3xx", "4xx", "5xx", "error"]).optional(),
|
||||
source: z.enum(["agent-bridge", "custom-host", "http-proxy", "system-proxy"]).optional(),
|
||||
sessionId: z.string().uuid().optional(),
|
||||
});
|
||||
46
src/shared/schemas/quota.ts
Normal file
46
src/shared/schemas/quota.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
import { PoolAllocationSchema, QuotaDimensionSchema } from "@/lib/quota/dimensions";
|
||||
|
||||
export const PoolCreateSchema = z.object({
|
||||
connectionId: z.string().min(1),
|
||||
name: z.string().min(1).max(120),
|
||||
allocations: z.array(PoolAllocationSchema).default([]),
|
||||
});
|
||||
export type PoolCreate = z.infer<typeof PoolCreateSchema>;
|
||||
|
||||
export const PoolUpdateSchema = z.object({
|
||||
name: z.string().min(1).max(120).optional(),
|
||||
allocations: z.array(PoolAllocationSchema).optional(),
|
||||
});
|
||||
export type PoolUpdate = z.infer<typeof PoolUpdateSchema>;
|
||||
|
||||
export const PlanUpsertSchema = z.object({
|
||||
dimensions: z.array(QuotaDimensionSchema).min(1),
|
||||
});
|
||||
export type PlanUpsert = z.infer<typeof PlanUpsertSchema>;
|
||||
|
||||
export const QuotaStoreSettingsSchema = z.object({
|
||||
driver: z.enum(["sqlite", "redis"]),
|
||||
redisUrl: z.string().url().nullable().optional(),
|
||||
});
|
||||
export type QuotaStoreSettings = z.infer<typeof QuotaStoreSettingsSchema>;
|
||||
|
||||
export const QuotaPreviewQuerySchema = z.object({
|
||||
apiKeyId: z.string().min(1),
|
||||
poolId: z.string().min(1),
|
||||
estimatedTokens: z.coerce.number().nonnegative().optional(),
|
||||
estimatedUsd: z.coerce.number().nonnegative().optional(),
|
||||
estimatedRequests: z.coerce.number().int().nonnegative().optional(),
|
||||
});
|
||||
export type QuotaPreviewQuery = z.infer<typeof QuotaPreviewQuerySchema>;
|
||||
|
||||
export const AuditLogQuerySchema = z.object({
|
||||
action: z.string().optional(),
|
||||
actor: z.string().optional(),
|
||||
level: z.enum(["high", "all"]).default("all"),
|
||||
from: z.string().datetime().optional(),
|
||||
to: z.string().datetime().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(500).default(50),
|
||||
offset: z.coerce.number().int().min(0).max(10_000).default(0),
|
||||
});
|
||||
export type AuditLogQuery = z.infer<typeof AuditLogQuerySchema>;
|
||||
@@ -12,6 +12,7 @@ import { extractApiKey } from "@/sse/services/auth";
|
||||
import { getApiKeyMetadata, getComboByName, isModelAllowedForKey } from "@/lib/localDb";
|
||||
import { resolveComboForModel } from "@/lib/db/modelComboMappings";
|
||||
import { checkBudget } from "@/domain/costRules";
|
||||
import { checkTokenLimits } from "@omniroute/open-sse/services/tokenLimitCounter.ts";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||
import * as log from "@/sse/utils/logger";
|
||||
@@ -394,6 +395,39 @@ export async function enforceApiKeyPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check 4.5: Per-model / per-provider token limits (Tier 1) ──
|
||||
if (apiKeyInfo.id) {
|
||||
try {
|
||||
const breach = checkTokenLimits(apiKeyInfo.id, undefined, modelStr ?? undefined);
|
||||
if (breach) {
|
||||
const scopeLabel =
|
||||
breach.scopeType === "global"
|
||||
? "account"
|
||||
: `${breach.scopeType} "${breach.scopeValue}"`;
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyInfo,
|
||||
rejection: errorResponse(
|
||||
HTTP_STATUS.RATE_LIMITED,
|
||||
`Token limit exceeded for ${scopeLabel}: ${breach.tokensUsed}/${breach.limitValue} tokens used in the current window. Please try again later.`
|
||||
),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Fail-closed: token-limit backend error should block the request,
|
||||
// consistent with the budget check above.
|
||||
log.error("API_POLICY", "Token limit check failed. Request blocked.", { error });
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyInfo,
|
||||
rejection: errorResponse(
|
||||
HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
"Token limit policy unavailable"
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check 5: Generic Multi-Window Rate Limits ──
|
||||
if (apiKeyInfo.id) {
|
||||
const hasCustomRateLimits = Boolean(apiKeyInfo.rateLimits && apiKeyInfo.rateLimits.length > 0);
|
||||
|
||||
@@ -452,8 +452,29 @@ export class CircuitBreakerOpenError extends Error {
|
||||
|
||||
// ─── Registry ─────────────────────────────────────
|
||||
|
||||
const MAX_REGISTRY_SIZE = 500;
|
||||
const registry = new Map<string, CircuitBreaker>();
|
||||
|
||||
const _registrySweep = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [name, breaker] of registry) {
|
||||
const status = breaker.getStatus();
|
||||
if (
|
||||
status.state === STATE.CLOSED &&
|
||||
status.failureCount === 0 &&
|
||||
(!status.lastFailureTime || now - status.lastFailureTime > 30 * 60 * 1000)
|
||||
) {
|
||||
registry.delete(name);
|
||||
try {
|
||||
deleteCircuitBreakerState(name);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}, 5 * 60_000);
|
||||
if (typeof _registrySweep === "object" && "unref" in _registrySweep) {
|
||||
(_registrySweep as { unref?: () => void }).unref?.();
|
||||
}
|
||||
|
||||
export function getCircuitBreaker(name: string, options?: CircuitBreakerOptions): CircuitBreaker {
|
||||
if (!registry.has(name)) {
|
||||
registry.set(name, new CircuitBreaker(name, options));
|
||||
|
||||
@@ -43,6 +43,8 @@ const QUOTA_PATTERNS: ReadonlyArray<RegExp> = [
|
||||
/out of credits/i,
|
||||
/hard.?limit/i,
|
||||
/plan.*limit/i,
|
||||
/resource.*exhaust/i,
|
||||
/check.*quota/i,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -429,6 +429,31 @@ export const importGeminiAuthSchema = z.object({
|
||||
overwriteExisting: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// ──── Antigravity CLI (`agy`) Auth Import Schema ────
|
||||
// Same source/options shape as gemini-cli; the parser handles the agy-specific token JSON.
|
||||
|
||||
export const importAgyAuthSchema = z.object({
|
||||
source: z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("json"), json: z.unknown() }),
|
||||
z.object({
|
||||
kind: z.literal("text"),
|
||||
text: z.string().max(256 * 1024, "agy token file content exceeds 256KB"),
|
||||
}),
|
||||
]),
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
email: z.string().email("Must be a valid email").optional(),
|
||||
overwriteExisting: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// ──── Antigravity CLI (`agy`) auto-detect local login Schema ────
|
||||
// No `source`: the route reads the token from the local agy CLI data dir on disk.
|
||||
|
||||
export const applyLocalAgyAuthSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
email: z.string().email("Must be a valid email").optional(),
|
||||
overwriteExisting: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// ──── Gemini CLI Auth Import Bulk Schema ────
|
||||
|
||||
export const importGeminiAuthBulkSchema = z.object({
|
||||
@@ -445,12 +470,28 @@ export const importGeminiAuthBulkSchema = z.object({
|
||||
overwriteExisting: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// ──── Antigravity CLI (`agy`) Auth Import Bulk Schema ────
|
||||
|
||||
export const importAgyAuthBulkSchema = z.object({
|
||||
entries: z
|
||||
.array(
|
||||
z.object({
|
||||
json: z.unknown(),
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
email: z.string().email("Must be a valid email").optional(),
|
||||
})
|
||||
)
|
||||
.min(1, "At least one entry is required")
|
||||
.max(50, "At most 50 entries per bulk import"),
|
||||
overwriteExisting: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// ──── API Key Schemas ────
|
||||
|
||||
export const createKeySchema = z.object({
|
||||
name: z.string().min(1, "Name is required").max(200),
|
||||
noLog: z.boolean().optional(),
|
||||
scopes: z.array(z.string().trim().min(1).max(64)).max(16).optional(),
|
||||
scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(),
|
||||
});
|
||||
|
||||
export const createSyncTokenSchema = z.object({
|
||||
@@ -586,6 +627,12 @@ const comboRuntimeConfigSchema = z
|
||||
failoverBeforeRetry: z.boolean().optional(),
|
||||
maxSetRetries: z.coerce.number().int().min(0).max(10).optional(),
|
||||
setRetryDelayMs: z.coerce.number().int().min(0).max(60000).optional(),
|
||||
zeroLatencyOptimizationsEnabled: z.boolean().optional(),
|
||||
hedging: z.boolean().optional(),
|
||||
hedgeDelayMs: z.coerce.number().int().min(0).max(60000).optional(),
|
||||
fallbackCompressionMode: compressionModeSchema.optional(),
|
||||
fallbackCompressionThreshold: z.coerce.number().int().min(0).max(2_000_000).optional(),
|
||||
predictiveTtftMs: z.coerce.number().int().min(0).max(300000).optional(),
|
||||
// Auto-Combo / LKGP Extensions
|
||||
candidatePool: z.array(z.string().min(1)).optional(),
|
||||
weights: scoringWeightsSchema.optional(),
|
||||
@@ -613,7 +660,29 @@ const comboRuntimeConfigSchema = z
|
||||
shadowRouting: shadowRoutingSchema.optional(),
|
||||
evalRouting: evalRoutingSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
.strict()
|
||||
.superRefine((config, ctx) => {
|
||||
if (config.zeroLatencyOptimizationsEnabled === true) return;
|
||||
|
||||
const addZeroLatencyIssue = (path: string[]) => {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"zeroLatencyOptimizationsEnabled must be true to enable zero-latency combo features",
|
||||
path,
|
||||
});
|
||||
};
|
||||
|
||||
if (config.hedging === true) {
|
||||
addZeroLatencyIssue(["hedging"]);
|
||||
}
|
||||
if (typeof config.predictiveTtftMs === "number" && config.predictiveTtftMs > 0) {
|
||||
addZeroLatencyIssue(["predictiveTtftMs"]);
|
||||
}
|
||||
if (config.fallbackCompressionMode && config.fallbackCompressionMode !== "off") {
|
||||
addZeroLatencyIssue(["fallbackCompressionMode"]);
|
||||
}
|
||||
});
|
||||
|
||||
const comboNameSchema = z
|
||||
.string()
|
||||
@@ -853,6 +922,34 @@ export const setBudgetSchema = z
|
||||
}
|
||||
});
|
||||
|
||||
export const setTokenLimitSchema = z
|
||||
.object({
|
||||
id: z.string().trim().min(1).optional(),
|
||||
apiKeyId: z.string().trim().min(1, "apiKeyId is required"),
|
||||
scopeType: z.enum(["model", "provider", "global"]),
|
||||
scopeValue: z.string().trim().default(""),
|
||||
tokenLimit: z.coerce
|
||||
.number()
|
||||
.int("tokenLimit must be an integer")
|
||||
.positive("tokenLimit must be greater than zero"),
|
||||
resetInterval: z.enum(["daily", "weekly", "monthly"]).default("monthly"),
|
||||
resetTime: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^\d{2}:\d{2}$/, "resetTime must be in HH:MM format")
|
||||
.optional(),
|
||||
enabled: z.boolean().default(true),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.scopeType !== "global" && (!value.scopeValue || value.scopeValue.length === 0)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "scopeValue is required unless scopeType is 'global'",
|
||||
path: ["scopeValue"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const policyActionSchema = z
|
||||
.object({
|
||||
action: z.enum(["unlock"]),
|
||||
@@ -1757,7 +1854,7 @@ export const updateKeyPermissionsSchema = z
|
||||
z.null(),
|
||||
])
|
||||
.optional(),
|
||||
scopes: z.array(z.string().trim().min(1).max(64)).max(16).optional(),
|
||||
scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(),
|
||||
allowedEndpoints: z.array(z.string().trim().min(1).max(64)).max(20).optional(),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
@@ -1996,7 +2093,8 @@ export const v1betaGeminiGenerateSchema = z
|
||||
});
|
||||
|
||||
export const cliMitmStartSchema = z.object({
|
||||
apiKey: z.string().trim().min(1, "Missing apiKey"),
|
||||
apiKey: z.string().trim().min(1).nullable().optional(),
|
||||
keyId: z.string().trim().min(1).nullable().optional(),
|
||||
sudoPassword: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -2270,3 +2368,17 @@ export const v1WebFetchSchema = z.object({
|
||||
wait_for_selector: z.string().max(256).optional(),
|
||||
include_metadata: z.boolean().default(false),
|
||||
});
|
||||
|
||||
// ── Zed Credential Import Flow ──────────────────────────────────────────────────
|
||||
|
||||
export const confirmedAccountSchema = z.object({
|
||||
service: z.string().min(1).max(500),
|
||||
account: z.string().min(1).max(500),
|
||||
fingerprint: z.string().min(1).max(100),
|
||||
});
|
||||
|
||||
export const zedImportSchema = z.object({
|
||||
confirmedAccounts: z.array(confirmedAccountSchema),
|
||||
});
|
||||
|
||||
export type ConfirmedAccount = z.infer<typeof confirmedAccountSchema>;
|
||||
|
||||
@@ -36,6 +36,11 @@ export const updateSettingsSchema = z.object({
|
||||
hideEndpointNgrokTunnel: z.boolean().optional(),
|
||||
autoRefreshProviderQuota: z.boolean().optional(),
|
||||
autoRefreshProviderQuotaInterval: z.number().int().min(10).max(3600).optional(),
|
||||
pinProviderQuotaToHome: z.boolean().optional(),
|
||||
showQuickStartOnHome: z.boolean().optional(),
|
||||
showProviderTopologyOnHome: z.boolean().optional(),
|
||||
localOnlyManageScopeBypassEnabled: z.boolean().optional(),
|
||||
localOnlyManageScopeBypassPrefixes: z.array(z.string().max(200)).optional(),
|
||||
debugMode: z.boolean().optional(),
|
||||
hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
|
||||
sidebarSectionOrder: z
|
||||
@@ -316,7 +321,7 @@ export const databaseSettingsSchema = z.object(
|
||||
// Aggregation settings
|
||||
aggregation: z.object({
|
||||
enabled: z.boolean(),
|
||||
rawDataRetentionDays: z.number().int().min(1).max(90),
|
||||
rawDataRetentionDays: z.number().int().min(1).max(3650),
|
||||
granularity: z.literal("hourly").or(z.literal("daily")).or(z.literal("weekly")),
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user