mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
chore: resolve merge conflicts in Dockerfile
This commit is contained in:
@@ -42,8 +42,8 @@ function validateKeyName(
|
||||
if (name.length > MAX_KEY_NAME_LENGTH) {
|
||||
return { valid: false, error: t("keyNameTooLong", { max: MAX_KEY_NAME_LENGTH }) };
|
||||
}
|
||||
// Only allow alphanumeric, spaces, hyphens, underscores
|
||||
if (!/^[a-zA-Z0-9_\-\s]+$/.test(name)) {
|
||||
// Allow Unicode letters (accented chars), numbers, spaces, hyphens, underscores
|
||||
if (!/^[\p{L}\p{N}_\-\s]+$/u.test(name)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: t("keyNameInvalid"),
|
||||
@@ -184,13 +184,17 @@ export default function ApiManagerPageClient() {
|
||||
const stats: Record<string, KeyUsageStats> = {};
|
||||
|
||||
for (const key of apiKeys) {
|
||||
// Match analytics entry by key name (reliable across both systems)
|
||||
const analyticsMatch = byApiKey.find((entry: any) => entry.apiKeyName === key.name);
|
||||
const analyticsMatch = byApiKey.find(
|
||||
(entry: any) =>
|
||||
entry.apiKeyId === key.id || (!entry.apiKeyId && entry.apiKeyName === key.name)
|
||||
);
|
||||
|
||||
// The call-logs endpoint returns entries sorted by timestamp DESC,
|
||||
// so the first match is the most recent one.
|
||||
const lastUsed =
|
||||
(logs || []).find((log: any) => log.apiKeyName === key.name)?.timestamp || null;
|
||||
(logs || []).find(
|
||||
(log: any) => log.apiKeyId === key.id || (!log.apiKeyId && log.apiKeyName === key.name)
|
||||
)?.timestamp || null;
|
||||
|
||||
stats[key.id] = {
|
||||
totalRequests: analyticsMatch?.requests ?? 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { IMAGE_PROVIDERS } from "@omniroute/open-sse/config/imageRegistry.ts";
|
||||
@@ -53,7 +53,7 @@ const MODALITY_CONFIG: Record<
|
||||
label: "Video Generation",
|
||||
placeholder: "A timelapse of a flower blooming...",
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
needsCredentials: [],
|
||||
needsCredentials: ["kie"],
|
||||
},
|
||||
music: {
|
||||
icon: "music_note",
|
||||
@@ -61,7 +61,7 @@ const MODALITY_CONFIG: Record<
|
||||
label: "Music Generation",
|
||||
placeholder: "Upbeat electronic music with synth pads...",
|
||||
color: "from-orange-500 to-yellow-500",
|
||||
needsCredentials: [],
|
||||
needsCredentials: ["kie"],
|
||||
},
|
||||
speech: {
|
||||
icon: "record_voice_over",
|
||||
@@ -89,6 +89,24 @@ const PROVIDER_MODELS: Record<
|
||||
> = {
|
||||
image: IMAGE_PROVIDER_MODELS,
|
||||
video: [
|
||||
{
|
||||
id: "kie",
|
||||
name: "KIE.AI",
|
||||
models: [
|
||||
{ id: "kie/veo/veo-3-1", name: "Veo 3.1" },
|
||||
{ id: "kie/veo/veo-3-1-fast", name: "Veo 3.1 Fast" },
|
||||
{ id: "kie/kling/kling-v2-1-master-text-to-video", name: "Kling v2.1 Master T2V" },
|
||||
{ id: "kie/kling/kling-v2-1-master-image-to-video", name: "Kling v2.1 Master I2V" },
|
||||
{ id: "kie/kling/v2-5-turbo-text-to-video", name: "Kling v2.5 Turbo T2V" },
|
||||
{ id: "kie/kling/v2-5-turbo-image-to-video", name: "Kling v2.5 Turbo I2V" },
|
||||
{ id: "kie/wan/2-7-text-to-video", name: "Wan 2.7 T2V" },
|
||||
{ id: "kie/wan/2-7-image-to-video", name: "Wan 2.7 I2V" },
|
||||
{ id: "kie/sora2/sora-2-text-to-video", name: "Sora 2 T2V" },
|
||||
{ id: "kie/hailuo/02-text-to-video-pro", name: "Hailuo 02 T2V Pro" },
|
||||
{ id: "kie/grok-imagine/text-to-video", name: "Grok Imagine T2V" },
|
||||
{ id: "kie/bytedance/v2-0-text-to-video", name: "Seedance v2.0 T2V" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "comfyui",
|
||||
name: "ComfyUI",
|
||||
@@ -104,6 +122,14 @@ const PROVIDER_MODELS: Record<
|
||||
},
|
||||
],
|
||||
music: [
|
||||
{
|
||||
id: "kie",
|
||||
name: "KIE.AI",
|
||||
models: [
|
||||
{ id: "kie/suno-v3.5", name: "Suno V3.5" },
|
||||
{ id: "kie/suno-v4.0", name: "Suno V4.0" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "comfyui",
|
||||
name: "ComfyUI",
|
||||
@@ -123,6 +149,16 @@ const PROVIDER_MODELS: Record<
|
||||
{ id: "openai/gpt-4o-mini-tts", name: "GPT-4o Mini TTS" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "kie",
|
||||
name: "KIE.AI",
|
||||
models: [
|
||||
{ id: "kie/elevenlabs/text-to-speech-multilingual-v2", name: "ElevenLabs TTS v2" },
|
||||
{ id: "kie/elevenlabs/text-to-speech-turbo-2-5", name: "ElevenLabs TTS Turbo 2.5" },
|
||||
{ id: "kie/elevenlabs/text-to-dialogue-v3", name: "ElevenLabs Text to Dialogue v3" },
|
||||
{ id: "kie/elevenlabs/sound-effect-v2", name: "ElevenLabs Sound Effect v2" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "elevenlabs",
|
||||
name: "ElevenLabs",
|
||||
@@ -204,6 +240,14 @@ const PROVIDER_MODELS: Record<
|
||||
{ id: "deepgram/base", name: "Base" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "kie",
|
||||
name: "KIE.AI",
|
||||
models: [
|
||||
{ id: "kie/elevenlabs/speech-to-text", name: "ElevenLabs STT" },
|
||||
{ id: "kie/elevenlabs/audio-isolation", name: "ElevenLabs Audio Isolation" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "assemblyai",
|
||||
name: "AssemblyAI ($50 free)",
|
||||
@@ -242,6 +286,8 @@ const PROVIDER_MODELS: Record<
|
||||
{ id: "qwen", name: "Qwen", models: [{ id: "qwen/qwen3-asr", name: "Qwen3 ASR" }] },
|
||||
],
|
||||
};
|
||||
const INITIAL_IMAGE_PROVIDER = PROVIDER_MODELS.image[0];
|
||||
const INITIAL_IMAGE_MODEL = INITIAL_IMAGE_PROVIDER?.models[0];
|
||||
|
||||
// Voice presets per TTS provider
|
||||
const VOICE_PRESETS: Record<string, { id: string; label: string }[]> = {
|
||||
@@ -264,6 +310,13 @@ const VOICE_PRESETS: Record<string, { id: string; label: string }[]> = {
|
||||
{ id: "pNInz6obpgDQGcFmaJgB", label: "Adam (EN)" },
|
||||
{ id: "yoZ06aMxZJJ28mfd3POQ", label: "Sam (EN)" },
|
||||
],
|
||||
kie: [
|
||||
{ id: "Rachel", label: "Rachel (EN)" },
|
||||
{ id: "Adam", label: "Adam (EN)" },
|
||||
{ id: "Brian", label: "Brian (EN)" },
|
||||
{ id: "Roger", label: "Roger (EN)" },
|
||||
{ id: "Bella", label: "Bella (EN)" },
|
||||
],
|
||||
cartesia: [
|
||||
{ id: "a0e99841-438c-4a64-b679-ae501e7d6091", label: "Barbershop Man" },
|
||||
{ id: "694f9389-aac1-45b6-b726-9d9369183238", label: "Friendly Reading Man" },
|
||||
@@ -433,8 +486,10 @@ export default function MediaPageClient() {
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
||||
// Selected provider and model per modality
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>("");
|
||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>(
|
||||
INITIAL_IMAGE_PROVIDER?.id ?? ""
|
||||
);
|
||||
const [selectedModel, setSelectedModel] = useState<string>(INITIAL_IMAGE_MODEL?.id ?? "");
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<GenerationResult | null>(null);
|
||||
@@ -536,16 +591,6 @@ export default function MediaPageClient() {
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on mount — pick first provider/model for image tab
|
||||
const initialized = useRef(false);
|
||||
if (!initialized.current) {
|
||||
initialized.current = true;
|
||||
const providers = PROVIDER_MODELS["image"] ?? [];
|
||||
const firstProvider = providers[0];
|
||||
setSelectedProvider(firstProvider?.id ?? "");
|
||||
setSelectedModel(firstProvider?.models[0]?.id ?? "");
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Badge, Modal, Input, ModelSelectModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
|
||||
export default function AntigravityToolCard({
|
||||
tool,
|
||||
isExpanded,
|
||||
@@ -226,17 +227,7 @@ export default function AntigravityToolCard({
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src={tool.image || "/providers/antigravity.png"}
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<ProviderIcon providerId={tool.id || "antigravity"} size={32} type="color" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
@@ -286,17 +286,7 @@ export default function ClaudeToolCard({
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src="/providers/claude.png"
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<ProviderIcon providerId="claude" size={32} type="color" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
@@ -241,23 +241,7 @@ export default function ClineToolCard({
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">
|
||||
{tool.image ? (
|
||||
<Image
|
||||
src={tool.image}
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="material-symbols-outlined text-xl" style={{ color: tool.color }}>
|
||||
terminal
|
||||
</span>
|
||||
)}
|
||||
<ProviderIcon providerId={tool.id || "cline"} size={32} type="color" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
|
||||
export default function CodexToolCard({
|
||||
tool,
|
||||
isExpanded,
|
||||
@@ -408,17 +409,7 @@ openai_base_url = "${getEffectiveBaseUrl()}"
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src="/providers/codex.png"
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<ProviderIcon providerId="codex" size={32} type="color" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { copyToClipboard } from "@/shared/utils/clipboard";
|
||||
import { buildOpenCodeConfigDocument } from "@/shared/services/opencodeConfig";
|
||||
import { useTheme } from "@/shared/hooks/useTheme";
|
||||
import { DEFAULT_DISPLAY_BASE_URL } from "@/shared/hooks";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
|
||||
export default function DefaultToolCard({
|
||||
toolId,
|
||||
@@ -659,19 +660,7 @@ export default function DefaultToolCard({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Image
|
||||
src={`/providers/${toolId}.png`}
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <ProviderIcon providerId={toolId} size={32} type="color" />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
export default function DroidToolCard({
|
||||
@@ -276,17 +277,7 @@ export default function DroidToolCard({
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src="/providers/droid.png"
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<ProviderIcon providerId="droid" size={32} type="color" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { createPortal } from "react-dom";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Card,
|
||||
@@ -49,6 +48,7 @@ import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases";
|
||||
import { maskEmail, pickMaskedDisplayValue, pickDisplayValue } from "@/shared/utils/maskEmail";
|
||||
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
|
||||
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
import {
|
||||
getClaudeCodeCompatibleRequestDefaults as _getClaudeCodeCompatibleRequestDefaults,
|
||||
getCodexRequestDefaults as _getCodexRequestDefaults,
|
||||
@@ -980,7 +980,6 @@ export default function ProviderDetailPage() {
|
||||
const [batchTesting, setBatchTesting] = useState(false);
|
||||
const [batchTestResults, setBatchTestResults] = useState<any>(null);
|
||||
const [modelAliases, setModelAliases] = useState({});
|
||||
const [headerImgError, setHeaderImgError] = useState(false);
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
const t = useTranslations("providers");
|
||||
const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible);
|
||||
@@ -2666,17 +2665,15 @@ export default function ProviderDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Determine icon path: OpenAI Compatible providers use specialized icons
|
||||
const getHeaderIconPath = () => {
|
||||
// OpenAI/Anthropic compatible providers use their specialized pseudo-provider icons.
|
||||
const getHeaderIconProviderId = () => {
|
||||
if (isOpenAICompatible && providerInfo.apiType) {
|
||||
return providerInfo.apiType === "responses"
|
||||
? "/providers/oai-r.png"
|
||||
: "/providers/oai-cc.png";
|
||||
return providerInfo.apiType === "responses" ? "oai-r" : "oai-cc";
|
||||
}
|
||||
if (isAnthropicProtocolCompatible) {
|
||||
return "/providers/anthropic-m.png";
|
||||
return "anthropic-m";
|
||||
}
|
||||
return `/providers/${providerInfo.id}.png`;
|
||||
return providerInfo.id;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -2695,21 +2692,7 @@ export default function ProviderDetailPage() {
|
||||
className="rounded-lg flex items-center justify-center"
|
||||
style={{ backgroundColor: `${providerInfo.color}15` }}
|
||||
>
|
||||
{headerImgError ? (
|
||||
<span className="text-sm font-bold" style={{ color: providerInfo.color }}>
|
||||
{providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()}
|
||||
</span>
|
||||
) : (
|
||||
<Image
|
||||
src={getHeaderIconPath()}
|
||||
alt={providerInfo.name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="object-contain rounded-lg max-w-[48px] max-h-[48px]"
|
||||
sizes="48px"
|
||||
onError={() => setHeaderImgError(true)}
|
||||
/>
|
||||
)}
|
||||
<ProviderIcon providerId={getHeaderIconProviderId()} size={48} type="color" />
|
||||
</div>
|
||||
<div>
|
||||
{providerInfo.website ? (
|
||||
@@ -6012,6 +5995,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
codexOpenaiStoreEnabled: false,
|
||||
consoleApiKey: "",
|
||||
ccCompatibleContext1m: false,
|
||||
geminiProjectId: "",
|
||||
blockExtraUsage:
|
||||
connection?.provider === "claude"
|
||||
? isClaudeExtraUsageBlockEnabled(connection?.provider, connection?.providerSpecificData)
|
||||
@@ -6037,6 +6021,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
const isCloudflare = connection?.provider === "cloudflare-ai";
|
||||
const isCodex = connection?.provider === "codex";
|
||||
const isClaude = connection?.provider === "claude";
|
||||
const isGeminiCli = connection?.provider === "gemini-cli";
|
||||
const localProviderMetadata = getLocalProviderMetadata(connection?.provider);
|
||||
const isLocalSelfHostedProvider = !!localProviderMetadata;
|
||||
const isSearxng = connection?.provider === "searxng-search";
|
||||
@@ -6099,6 +6084,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
|
||||
consoleApiKey: existingConsoleApiKey,
|
||||
ccCompatibleContext1m: ccRequestDefaults.context1m,
|
||||
geminiProjectId: (connection.providerSpecificData?.projectId as string) || "",
|
||||
blockExtraUsage: isClaudeExtraUsageBlockEnabled(
|
||||
connection.provider,
|
||||
connection.providerSpecificData
|
||||
@@ -6205,6 +6191,10 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
healthCheckInterval: formData.healthCheckInterval,
|
||||
};
|
||||
|
||||
if (isGeminiCli) {
|
||||
updates.projectId = formData.geminiProjectId.trim() || null;
|
||||
}
|
||||
|
||||
if (isGooglePse && !formData.cx.trim()) {
|
||||
setSaveError(t("searchEngineIdRequired"));
|
||||
return;
|
||||
@@ -6326,6 +6316,9 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
updates.providerSpecificData.openaiStoreEnabled =
|
||||
formData.codexOpenaiStoreEnabled === true;
|
||||
}
|
||||
if (isGeminiCli) {
|
||||
updates.providerSpecificData.projectId = formData.geminiProjectId.trim() || undefined;
|
||||
}
|
||||
}
|
||||
const error = (await onSave(updates)) as void | unknown;
|
||||
if (error) {
|
||||
@@ -6420,6 +6413,18 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isGeminiCli && (
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
|
||||
<Input
|
||||
label={t("geminiCliProjectIdLabel")}
|
||||
value={formData.geminiProjectId}
|
||||
onChange={(e) => setFormData({ ...formData, geminiProjectId: e.target.value })}
|
||||
placeholder={t("geminiCliProjectIdPlaceholder")}
|
||||
hint={t("geminiCliProjectIdHint")}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isOAuth && connection.email && (
|
||||
<div className="bg-sidebar/50 p-3 rounded-lg">
|
||||
<p className="text-sm text-text-muted mb-1">{t("email")}</p>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
parseQuotaData,
|
||||
calculatePercentage,
|
||||
@@ -18,6 +17,7 @@ import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { pickMaskedDisplayValue, pickDisplayValue } from "@/shared/utils/maskEmail";
|
||||
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
|
||||
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
|
||||
const LS_GROUP_BY = "omniroute:limits:groupBy";
|
||||
const LS_EXPANDED_GROUPS = "omniroute:limits:expandedGroups";
|
||||
@@ -551,14 +551,7 @@ export default function ProviderLimits() {
|
||||
{/* Account Info */}
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center overflow-hidden shrink-0">
|
||||
<Image
|
||||
src={`/providers/${conn.provider}.png`}
|
||||
alt={conn.provider}
|
||||
width={32}
|
||||
height={32}
|
||||
className="object-contain"
|
||||
sizes="32px"
|
||||
/>
|
||||
<ProviderIcon providerId={conn.provider} size={32} type="color" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[13px] font-semibold text-text-main truncate">
|
||||
|
||||
@@ -126,6 +126,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
healthCheckInterval,
|
||||
group,
|
||||
maxConcurrent,
|
||||
projectId,
|
||||
providerSpecificData: incomingPsd,
|
||||
} = body;
|
||||
|
||||
@@ -152,6 +153,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
if (healthCheckInterval !== undefined) updateData.healthCheckInterval = healthCheckInterval;
|
||||
if (group !== undefined) updateData.group = group;
|
||||
if (maxConcurrent !== undefined) updateData.maxConcurrent = maxConcurrent;
|
||||
if (projectId !== undefined) updateData.projectId = projectId;
|
||||
|
||||
// Merge providerSpecificData (partial update — preserve existing keys not sent by caller)
|
||||
if (incomingPsd !== undefined && incomingPsd !== null && typeof incomingPsd === "object") {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeys } from "@/lib/db/apiKeys";
|
||||
import { getDbInstance } from "@/lib/db/core";
|
||||
|
||||
function getRangeStartIso(range: string): string | null {
|
||||
@@ -76,6 +77,16 @@ function uniqueValues(values: Array<string | null | undefined>): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeApiKeyUsageGroup(apiKeyId: string, fallbackName: string): string {
|
||||
return apiKeyId ? `id:${apiKeyId}` : `name:${fallbackName}`;
|
||||
}
|
||||
|
||||
function addApiKeyAlias(target: Set<string>, value: unknown): void {
|
||||
if (typeof value !== "string") return;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed) target.add(trimmed);
|
||||
}
|
||||
|
||||
function stripCodexEffortSuffix(model: string): string {
|
||||
return model.replace(/-(?:xhigh|high|medium|low|none)$/i, "");
|
||||
}
|
||||
@@ -244,6 +255,13 @@ export async function GET(request: Request) {
|
||||
const presetsParam = searchParams.get("presets");
|
||||
|
||||
const db = getDbInstance();
|
||||
const apiKeys = await getApiKeys();
|
||||
const currentApiKeyNames = new Map<string, string>();
|
||||
for (const apiKey of apiKeys) {
|
||||
if (typeof apiKey.id === "string" && typeof apiKey.name === "string") {
|
||||
currentApiKeyNames.set(apiKey.id, apiKey.name);
|
||||
}
|
||||
}
|
||||
|
||||
const conditions = [];
|
||||
const params: Record<string, string> = {};
|
||||
@@ -296,7 +314,7 @@ export async function GET(request: Request) {
|
||||
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens,
|
||||
COUNT(DISTINCT model) as uniqueModels,
|
||||
COUNT(DISTINCT connection_id) as uniqueAccounts,
|
||||
COUNT(DISTINCT api_key_id) as uniqueApiKeys,
|
||||
COUNT(DISTINCT COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''))) as uniqueApiKeys,
|
||||
COALESCE(SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END), 0) as successfulRequests,
|
||||
COALESCE(AVG(latency_ms), 0) as avgLatencyMs,
|
||||
COALESCE(MIN(timestamp), '') as firstRequest,
|
||||
@@ -489,8 +507,8 @@ export async function GET(request: Request) {
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
api_key_id as apiKeyId,
|
||||
COALESCE(NULLIF(api_key_name, ''), NULLIF(api_key_id, ''), 'Unknown API key') as apiKeyName,
|
||||
NULLIF(api_key_id, '') as apiKeyId,
|
||||
COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown') as apiKeyGroupKey,
|
||||
LOWER(provider) as provider,
|
||||
LOWER(model) as model,
|
||||
COUNT(*) as requests,
|
||||
@@ -502,11 +520,42 @@ export async function GET(request: Request) {
|
||||
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
|
||||
FROM usage_history
|
||||
${apiKeyWhereClause}
|
||||
GROUP BY api_key_id, api_key_name, LOWER(provider), LOWER(model)
|
||||
GROUP BY COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown'), NULLIF(api_key_id, ''), LOWER(provider), LOWER(model)
|
||||
`
|
||||
)
|
||||
.all(params) as Array<Record<string, unknown>>;
|
||||
|
||||
const apiKeyMetadataRows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
NULLIF(api_key_id, '') as apiKeyId,
|
||||
NULLIF(api_key_name, '') as apiKeyName,
|
||||
COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown') as apiKeyGroupKey,
|
||||
MAX(timestamp) as lastUsed
|
||||
FROM usage_history
|
||||
${apiKeyWhereClause}
|
||||
GROUP BY NULLIF(api_key_id, ''), NULLIF(api_key_name, '')
|
||||
ORDER BY lastUsed DESC
|
||||
`
|
||||
)
|
||||
.all(params) as Array<Record<string, unknown>>;
|
||||
|
||||
const apiKeyMetadata = new Map<string, { latestName: string; aliases: Set<string> }>();
|
||||
for (const row of apiKeyMetadataRows) {
|
||||
const apiKeyId = toStringValue(row.apiKeyId);
|
||||
const apiKeyGroupKey = toStringValue(row.apiKeyGroupKey, "unknown");
|
||||
const groupKey = makeApiKeyUsageGroup(apiKeyId, apiKeyGroupKey);
|
||||
const existing = apiKeyMetadata.get(groupKey) || {
|
||||
latestName: "",
|
||||
aliases: new Set<string>(),
|
||||
};
|
||||
const apiKeyName = toStringValue(row.apiKeyName);
|
||||
if (!existing.latestName && apiKeyName) existing.latestName = apiKeyName;
|
||||
addApiKeyAlias(existing.aliases, apiKeyName);
|
||||
apiKeyMetadata.set(groupKey, existing);
|
||||
}
|
||||
|
||||
const weeklyRows = db
|
||||
.prepare(
|
||||
`
|
||||
@@ -742,6 +791,7 @@ export async function GET(request: Request) {
|
||||
apiKey: string;
|
||||
apiKeyId: string | null;
|
||||
apiKeyName: string;
|
||||
historicalApiKeyNames: string[];
|
||||
requests: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
@@ -751,12 +801,20 @@ export async function GET(request: Request) {
|
||||
>();
|
||||
for (const row of apiKeyRows) {
|
||||
const apiKeyId = toStringValue(row.apiKeyId);
|
||||
const apiKeyName = toStringValue(row.apiKeyName, apiKeyId || "Unknown API key");
|
||||
const key = `${apiKeyId || "unknown"}::${apiKeyName}`;
|
||||
const apiKeyGroupKey = toStringValue(row.apiKeyGroupKey, "unknown");
|
||||
const key = makeApiKeyUsageGroup(apiKeyId, apiKeyGroupKey);
|
||||
const metadata = apiKeyMetadata.get(key);
|
||||
const apiKeyName =
|
||||
(apiKeyId ? currentApiKeyNames.get(apiKeyId) : undefined) ||
|
||||
metadata?.latestName ||
|
||||
apiKeyId ||
|
||||
apiKeyGroupKey ||
|
||||
"Unknown API key";
|
||||
const existing = apiKeyMap.get(key) || {
|
||||
apiKey: apiKeyId && apiKeyName !== apiKeyId ? `${apiKeyName} (${apiKeyId})` : apiKeyName,
|
||||
apiKeyId: apiKeyId || null,
|
||||
apiKeyName,
|
||||
historicalApiKeyNames: Array.from(metadata?.aliases || []),
|
||||
requests: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getAllModerationModels } from "@omniroute/open-sse/config/moderationReg
|
||||
import { getAllVideoModels } from "@omniroute/open-sse/config/videoRegistry.ts";
|
||||
import { getAllMusicModels } from "@omniroute/open-sse/config/musicRegistry.ts";
|
||||
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
import { CODEX_NATIVE_UNPREFIXED_MODELS } from "@omniroute/open-sse/services/model.ts";
|
||||
import { getAllSyncedAvailableModels } from "@/lib/db/models";
|
||||
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
|
||||
import { hasEligibleConnectionForModel } from "@/domain/connectionModelRules";
|
||||
@@ -25,6 +26,8 @@ import {
|
||||
getCatalogDiagnosticsHeaders,
|
||||
} from "@/lib/modelMetadataRegistry";
|
||||
import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { parseModel } from "@omniroute/open-sse/services/model.ts";
|
||||
import { getTokenLimit } from "@omniroute/open-sse/services/contextManager.ts";
|
||||
|
||||
const FALLBACK_ALIAS_TO_PROVIDER = {
|
||||
ag: "antigravity",
|
||||
@@ -313,6 +316,30 @@ export async function getUnifiedModelsResponse(
|
||||
// Add combos first (they appear at the top) — only active ones
|
||||
for (const combo of combos) {
|
||||
if (combo.isActive === false || combo.isHidden === true) continue;
|
||||
|
||||
// Calculate combo context length from its model targets.
|
||||
// OpenCode and other clients read context_length from the catalog; without it
|
||||
// they fall back to a conservative ~4000 token limit, causing truncation.
|
||||
const comboContextLength = Array.isArray(combo.models)
|
||||
? combo.models
|
||||
.filter((step) => step && step.kind === "model" && step.model)
|
||||
.map((step) => {
|
||||
const parsed = parseModel(step.model);
|
||||
const provider = parsed.provider || (step as any).providerId || "unknown";
|
||||
const model = parsed.model || step.model;
|
||||
return getTokenLimit(provider, model);
|
||||
})
|
||||
.filter((limit): limit is number => typeof limit === "number" && limit > 0)
|
||||
.reduce((min, limit) => Math.min(min, limit), Infinity)
|
||||
: undefined;
|
||||
|
||||
const effectiveContextLength =
|
||||
typeof combo.context_length === "number" && combo.context_length > 0
|
||||
? combo.context_length
|
||||
: comboContextLength !== undefined && comboContextLength !== Infinity
|
||||
? comboContextLength
|
||||
: undefined;
|
||||
|
||||
models.push({
|
||||
id: combo.name,
|
||||
object: "model",
|
||||
@@ -321,7 +348,7 @@ export async function getUnifiedModelsResponse(
|
||||
permission: [],
|
||||
root: combo.name,
|
||||
parent: null,
|
||||
...(combo.context_length ? { context_length: combo.context_length } : {}),
|
||||
...(effectiveContextLength !== undefined ? { context_length: effectiveContextLength } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -377,6 +404,33 @@ export async function getUnifiedModelsResponse(
|
||||
}
|
||||
}
|
||||
|
||||
for (const modelId of CODEX_NATIVE_UNPREFIXED_MODELS) {
|
||||
if (!providerSupportsModel("codex", modelId)) continue;
|
||||
if (getModelIsHidden("codex", modelId)) continue;
|
||||
|
||||
const alias = providerIdToAlias.codex || "cx";
|
||||
const aliasId = `${alias}/${modelId}`;
|
||||
const providerIdModel = `codex/${modelId}`;
|
||||
const entries = [
|
||||
{ id: aliasId, parent: null },
|
||||
{ id: providerIdModel, parent: aliasId },
|
||||
{ id: modelId, parent: providerIdModel },
|
||||
];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (models.some((existingModel) => existingModel.id === entry.id)) continue;
|
||||
models.push({
|
||||
id: entry.id,
|
||||
object: "model",
|
||||
created: timestamp,
|
||||
owned_by: "codex",
|
||||
permission: [],
|
||||
root: modelId,
|
||||
parent: entry.parent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const syncedModelsByProvider = await getAllSyncedAvailableModels();
|
||||
for (const [providerId, syncedModels] of Object.entries(syncedModelsByProvider)) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export function DocsSidebarClient({ mobileOnly = false }: { mobileOnly?: boolean
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Extract slug from pathname (e.g., /docs/setup-guide -> setup-guide)
|
||||
const currentSlug = pathname.split("/").filter(Boolean).pop() || "";
|
||||
const currentSlug = pathname.split("/").filter(Boolean).pop();
|
||||
|
||||
const isActive = (slug: string) => currentSlug === slug;
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
|
||||
export default function FlowAnimation() {
|
||||
const t = useTranslations("landing");
|
||||
const [activeFlow, setActiveFlow] = useState(0);
|
||||
|
||||
const cliTools = [
|
||||
{ id: "claude", name: t("flowToolClaudeCode"), image: "/providers/claude.png" },
|
||||
{ id: "codex", name: t("flowToolOpenAICodex"), image: "/providers/codex.png" },
|
||||
{ id: "cline", name: t("flowToolCline"), image: "/providers/cline.png" },
|
||||
{ id: "cursor", name: t("flowToolCursor"), image: "/providers/cursor.png" },
|
||||
{ id: "claude", name: t("flowToolClaudeCode") },
|
||||
{ id: "codex", name: t("flowToolOpenAICodex") },
|
||||
{ id: "cline", name: t("flowToolCline") },
|
||||
{ id: "cursor", name: t("flowToolCursor") },
|
||||
];
|
||||
|
||||
const providers = [
|
||||
@@ -70,14 +71,7 @@ export default function FlowAnimation() {
|
||||
className="flex items-center gap-3 opacity-70 hover:opacity-100 transition-opacity group"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-2xl bg-[#111520] border border-[#2D333B] flex items-center justify-center overflow-hidden p-2 hover:border-[#E54D5E]/50 transition-all hover:scale-105">
|
||||
<Image
|
||||
src={tool.image}
|
||||
alt={tool.name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="object-contain rounded-xl max-w-[48px] max-h-[48px]"
|
||||
sizes="48px"
|
||||
/>
|
||||
<ProviderIcon providerId={tool.id} size={48} type="color" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user