chore: resolve merge conflicts in Dockerfile

This commit is contained in:
diegosouzapw
2026-05-07 08:59:02 -03:00
204 changed files with 3651 additions and 1131 deletions

View File

@@ -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,

View File

@@ -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);

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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 (

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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") {

View File

@@ -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,

View File

@@ -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)) {

View File

@@ -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;

View File

@@ -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>
))}