diff --git a/open-sse/services/autoCombo/engine.ts b/open-sse/services/autoCombo/engine.ts index 48d316c86f..74c85b896a 100644 --- a/open-sse/services/autoCombo/engine.ts +++ b/open-sse/services/autoCombo/engine.ts @@ -173,35 +173,6 @@ export function selectProvider( }; } -// ============ In-Memory Auto-Combo Registry ============ - -const autoCombos = new Map(); - -export function createAutoCombo(config: Omit): AutoComboConfig { - const full: AutoComboConfig = { ...config, type: "auto" }; - autoCombos.set(config.id, full); - return full; -} - -export function getAutoCombo(id: string): AutoComboConfig | undefined { - return autoCombos.get(id); -} - -export function updateAutoCombo( - id: string, - update: Partial -): AutoComboConfig | undefined { - const existing = autoCombos.get(id); - if (!existing) return undefined; - const updated = { ...existing, ...update, id, type: "auto" as const }; - autoCombos.set(id, updated); - return updated; -} - -export function deleteAutoCombo(id: string): boolean { - return autoCombos.delete(id); -} - -export function listAutoCombos(): AutoComboConfig[] { - return [...autoCombos.values()]; -} +// ============ Auto-Combo Config Schema Reference ============ +// Note: AutoCombos are now persisted natively in the SQLite DB via src/lib/db/combos.ts +// using the combo.strategy = "auto" | "lkgp" type, with parameters nested inside combo.config diff --git a/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx b/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx index 8d48e93436..e54fdf5f0c 100644 --- a/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx +++ b/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx @@ -90,19 +90,27 @@ function getLatestPoints(points: ProviderUtilizationPoint[]) { export default function ProviderUtilizationTab() { const [range, setRange] = useState("24h"); + const [aggregateBy, setAggregateBy] = useState<"provider" | "connection">("provider"); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchUtilization = useCallback( - async (selectedRange: UtilizationTimeRange, signal?: AbortSignal) => { + async ( + selectedRange: UtilizationTimeRange, + selectedAggregate: "provider" | "connection", + signal?: AbortSignal + ) => { setLoading(true); try { - const response = await fetch(`/api/usage/utilization?range=${selectedRange}`, { - signal, - cache: "no-store", - }); + const response = await fetch( + `/api/usage/utilization?range=${selectedRange}&aggregateBy=${selectedAggregate}`, + { + signal, + cache: "no-store", + } + ); if (!response.ok) { throw new Error("Failed to fetch utilization data"); @@ -132,10 +140,10 @@ export default function ProviderUtilizationTab() { useEffect(() => { const controller = new AbortController(); - fetchUtilization(range, controller.signal); + fetchUtilization(range, aggregateBy, controller.signal); return () => controller.abort(); - }, [fetchUtilization, range]); + }, [fetchUtilization, range, aggregateBy]); const providerColors = useMemo(() => { const colors = new Map(); @@ -177,8 +185,8 @@ export default function ProviderUtilizationTab() { const handleRetry = useCallback(() => { setRetrying(true); setError(null); - fetchUtilization(range).finally(() => setRetrying(false)); - }, [range, fetchUtilization]); + fetchUtilization(range, aggregateBy).finally(() => setRetrying(false)); + }, [range, aggregateBy, fetchUtilization]); return (
@@ -186,7 +194,35 @@ export default function ProviderUtilizationTab() { title="Provider utilization" subtitle={RANGE_LABELS[range]} icon="monitoring" - action={} + action={ +
+
+ + +
+ +
+ } className="overflow-hidden" > {loading && !hasData ? ( diff --git a/src/app/(dashboard)/dashboard/auto-combo/AutoComboModal.tsx b/src/app/(dashboard)/dashboard/auto-combo/AutoComboModal.tsx new file mode 100644 index 0000000000..b080f567b8 --- /dev/null +++ b/src/app/(dashboard)/dashboard/auto-combo/AutoComboModal.tsx @@ -0,0 +1,161 @@ +import { useState, useEffect } from "react"; +import { Modal, Input, Button } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +export default function AutoComboModal({ isOpen, onClose, onSave, combo, activeProviders = [] }) { + const t = useTranslations("combos"); + const tc = useTranslations("common"); + const [formData, setFormData] = useState({ + name: "", + strategy: "auto", + candidatePool: [], + explorationRate: 0.05, + modePack: "ship-fast", + budgetCap: "", + }); + + useEffect(() => { + if (combo) { + // eslint-disable-next-line + setFormData({ + name: combo.name || "", + strategy: combo.strategy || "auto", + candidatePool: combo.config?.candidatePool || [], + explorationRate: combo.config?.explorationRate ?? 0.05, + modePack: combo.config?.modePack || "ship-fast", + budgetCap: combo.config?.budgetCap || "", + }); + } else { + + setFormData({ + name: "", + strategy: "auto", + candidatePool: [], + explorationRate: 0.05, + modePack: "ship-fast", + budgetCap: "", + }); + } + }, [combo, isOpen]); + + const handleSubmit = (e) => { + e.preventDefault(); + onSave({ + name: formData.name, + strategy: formData.strategy, + config: { + candidatePool: formData.candidatePool, + explorationRate: Number(formData.explorationRate), + modePack: formData.modePack, + budgetCap: formData.budgetCap ? Number(formData.budgetCap) : undefined, + }, + }); + }; + + const handleProviderToggle = (providerId) => { + setFormData((prev) => { + const pool = prev.candidatePool.includes(providerId) + ? prev.candidatePool.filter((id) => id !== providerId) + : [...prev.candidatePool, providerId]; + return { ...prev, candidatePool: pool }; + }); + }; + + return ( + +
+ setFormData({ ...formData, name: e.target.value })} + required + pattern="^[a-zA-Z0-9_\/\.\-]+$" + disabled={!!combo} // Cannot change name if editing + /> + +
+ + +
+ +
+ +

+ Select which providers this engine evaluates. +

+
+ {activeProviders.map((p) => ( + + ))} + {activeProviders.length === 0 && ( + No active APIs found + )} +
+
+ +
+ setFormData({ ...formData, explorationRate: e.target.value })} + /> +
+ + +
+
+ + setFormData({ ...formData, budgetCap: e.target.value })} + /> + +
+ + +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/auto-combo/page.tsx b/src/app/(dashboard)/dashboard/auto-combo/page.tsx index 022b9cd96a..bf2cc1928d 100644 --- a/src/app/(dashboard)/dashboard/auto-combo/page.tsx +++ b/src/app/(dashboard)/dashboard/auto-combo/page.tsx @@ -7,7 +7,9 @@ "use client"; import { useEffect, useState, useCallback } from "react"; -import { Card } from "@/shared/components"; +import { Card, Button } from "@/shared/components"; +import AutoComboModal from "./AutoComboModal"; +import { useNotificationStore } from "@/store/notificationStore"; interface ProviderScore { provider: string; @@ -44,21 +46,25 @@ export default function AutoComboDashboard() { const [incidentMode, setIncidentMode] = useState(false); const [modePack, setModePack] = useState("ship-fast"); - const fetchData = useCallback(async () => { - try { - const [combosRes, healthRes] = await Promise.allSettled([ - fetch("/api/combos/auto"), - fetch("/api/monitoring/health"), - ]); + const notify = useNotificationStore(); + const [combos, setCombos] = useState([]); + const [showCreateModal, setShowCreateModal] = useState(false); + const [editingCombo, setEditingCombo] = useState(null); + const [activeProviders, setActiveProviders] = useState([]); - if (combosRes.status === "fulfilled") { - const comboPayload = await combosRes.value.json(); - const combos = Array.isArray(comboPayload?.combos) - ? (comboPayload.combos as AutoComboRecord[]) - : []; - const firstCombo = combos[0] || null; - const candidatePool = Array.isArray(firstCombo?.candidatePool) - ? firstCombo.candidatePool.filter((entry): entry is string => typeof entry === "string") + const fetchCombos = useCallback(async () => { + try { + const res = await fetch("/api/combos"); + if (res.ok) { + const payload = await res.json(); + const allCombos = Array.isArray(payload?.combos) ? payload.combos : []; + const auto = allCombos.filter((c: any) => c.strategy === "auto" || c.strategy === "lkgp"); + setCombos(auto); + + // Refresh scores based on first auto combo found + const firstCombo = auto[0] || null; + const candidatePool = Array.isArray(firstCombo?.config?.candidatePool) + ? firstCombo.config.candidatePool : []; const rawWeights = firstCombo?.weights && @@ -81,9 +87,16 @@ export default function AutoComboDashboard() { } else { setScores([]); } + } catch { + setScores([]); + } + }, []); - if (healthRes.status === "fulfilled") { - const health = (await healthRes.value.json()) as HealthRecord; + const fetchHealth = useCallback(async () => { + try { + const healthRes = await fetch("/api/monitoring/health"); + if (healthRes.ok) { + const health = (await healthRes.json()) as HealthRecord; const providerHealth = health?.providerHealth && typeof health.providerHealth === "object" ? health.providerHealth @@ -126,6 +139,23 @@ export default function AutoComboDashboard() { } }, []); + const fetchData = useCallback(async () => { + await Promise.all([fetchCombos(), fetchHealth()]); + + // Fetch active providers for the Modal + try { + const pRes = await fetch("/api/providers"); + if (pRes.ok) { + const pData = await pRes.json(); + setActiveProviders( + (pData.connections || []).filter( + (c: any) => c.testStatus === "active" || c.testStatus === "success" + ) + ); + } + } catch {} + }, [fetchCombos, fetchHealth]); + useEffect(() => { const id = setTimeout(fetchData, 0); const interval = setInterval(fetchData, 30_000); @@ -135,6 +165,59 @@ export default function AutoComboDashboard() { }; }, [fetchData]); + const handleCreate = async (data: any) => { + try { + const res = await fetch("/api/combos", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (res.ok) { + await fetchCombos(); + setShowCreateModal(false); + notify.success("Auto-Combo created successfully"); + } else { + const err = await res.json(); + notify.error(err.error?.message || err.error || "Failed to create combo"); + } + } catch { + notify.error("Error creating combo"); + } + }; + + const handleUpdate = async (id: string, data: any) => { + try { + const res = await fetch(`/api/combos/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (res.ok) { + await fetchCombos(); + setEditingCombo(null); + notify.success("Auto-Combo updated"); + } else { + const err = await res.json(); + notify.error("Failed to update: " + (err.error?.message || err.error)); + } + } catch { + notify.error("Error updating combo"); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Are you sure you want to delete this auto-combo?")) return; + try { + const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); + if (res.ok) { + setCombos(combos.filter((c) => c.id !== id)); + notify.success("Auto-combo deleted"); + } + } catch { + notify.error("Error deleting combo"); + } + }; + const FACTOR_LABELS: Record = { quota: "📊 Quota", health: "💚 Health", @@ -154,15 +237,74 @@ export default function AutoComboDashboard() { return (
-
+

âš¡ Auto-Combo Engine

Smart routing automatically adapting to latency, health, and throughput

+
+ {/* ──── CRUD Auto Combos List ──── */} + {combos.length > 0 && ( + +

Configured Auto-Combos

+
+ {combos.map((combo) => ( +
+
+

+ {combo.name} + + {combo.strategy} + +

+

+ Pool: {combo.config?.candidatePool?.length || "All"} APIs | Pack:{" "} + {combo.config?.modePack || "fast"} +

+
+
+ + +
+
+ ))} +
+
+ )} + + {/* Forms */} + {showCreateModal && ( + setShowCreateModal(false)} + onSave={handleCreate} + activeProviders={activeProviders} + combo={null} + /> + )} + {editingCombo && ( + setEditingCombo(null)} + onSave={(data: any) => handleUpdate(editingCombo.id, data)} + activeProviders={activeProviders} + combo={editingCombo} + /> + )} +
@@ -242,8 +384,7 @@ export default function AutoComboDashboard() { {scores.length === 0 ? (

- No auto-combo configured or data loading... Create one via{" "} - POST /api/combos/auto. + No auto-combo configured... Create one to see live provider scores.

) : (
diff --git a/src/app/api/combos/auto/route.ts b/src/app/api/combos/auto/route.ts deleted file mode 100644 index 694df34149..0000000000 --- a/src/app/api/combos/auto/route.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Auto-Combo REST API — `/api/combos/auto` - * - * POST — Create auto-combo - * GET — List all auto-combos - * - * Note: Auto-combo state is managed in-memory by the engine module. - * The open-sse/services/autoCombo module is outside Next.js src/, - * so we use a lightweight in-memory store here that mirrors the engine API. - */ - -import { NextRequest, NextResponse } from "next/server"; -import { createAutoComboSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; - -// ── In-memory auto-combo store (mirrors open-sse/services/autoCombo/engine.ts) ── - -interface ScoringWeights { - quota: number; - health: number; - costInv: number; - latencyInv: number; - taskFit: number; - stability: number; - tierPriority: number; -} - -const DEFAULT_WEIGHTS: ScoringWeights = { - quota: 0.2, - health: 0.25, - costInv: 0.2, - latencyInv: 0.15, - taskFit: 0.1, - stability: 0.05, - tierPriority: 0.05, -}; - -interface AutoComboConfig { - id: string; - name: string; - type: "auto"; - candidatePool: string[]; - weights: ScoringWeights; - modePack?: string; - budgetCap?: number; - explorationRate: number; -} - -const autoCombos = new Map(); - -export async function POST(req: NextRequest) { - let rawBody: unknown; - try { - rawBody = await req.json(); - } catch { - return NextResponse.json( - { - error: { - message: "Invalid request", - details: [{ field: "body", message: "Invalid JSON body" }], - }, - }, - { status: 400 } - ); - } - - try { - const validation = validateBody(createAutoComboSchema, rawBody); - if (isValidationFailure(validation)) { - return NextResponse.json({ error: validation.error }, { status: 400 }); - } - const { id, name, candidatePool, weights, modePack, budgetCap, explorationRate } = - validation.data; - - const config: AutoComboConfig = { - id, - name, - type: "auto", - candidatePool, - weights: weights ?? DEFAULT_WEIGHTS, - modePack, - budgetCap, - explorationRate, - }; - autoCombos.set(id, config); - - return NextResponse.json(config, { status: 201 }); - } catch (err) { - console.log("Error creating auto-combo:", err); - return NextResponse.json({ error: "Failed to create auto-combo" }, { status: 500 }); - } -} - -export async function GET() { - return NextResponse.json({ combos: [...autoCombos.values()] }); -} diff --git a/src/app/api/usage/utilization/route.ts b/src/app/api/usage/utilization/route.ts index df18fa455f..4d602fefe2 100644 --- a/src/app/api/usage/utilization/route.ts +++ b/src/app/api/usage/utilization/route.ts @@ -43,11 +43,14 @@ export async function GET(request: Request) { const range = rangeParam as UtilizationTimeRange; const since = getRangeStartIso(range); const bucketMinutes = BUCKET_SIZES[range]; + const aggregateByParam = searchParams.get("aggregateBy"); + const aggregateBy = aggregateByParam === "connection" ? "connection" : "provider"; const data = getAggregatedSnapshots({ provider: providerParam || undefined, since, bucketMinutes, + aggregateBy, }); const providers = Array.from(new Set(data.map((d) => d.provider))); diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 2f5b0a2d23..ebabd3eb3c 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -994,7 +994,11 @@ } }, "templateFreeStack": "Free Stack ($0)", - "templateFreeStackDesc": "Round-robin across all free providers: Kiro (Claude), Qoder (5 models), Qwen (4 models), Gemini CLI. Zero cost, never stops coding." + "templateFreeStackDesc": "Round-robin across all free providers: Kiro (Claude), Qoder (5 models), Qwen (4 models), Gemini CLI. Zero cost, never stops coding.", + "auto": "Auto Combo", + "autoDesc": "Self-healing smart routing pool (Performance optimized)", + "lkgp": "LKGP Mode", + "lkgpDesc": "Last Known Good Provider (Predictable resilience)" }, "costs": { "title": "Costs", @@ -1778,7 +1782,10 @@ "chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)", "modelsPathLabel": "Models Endpoint Path", "modelsPathPlaceholder": "/models", - "modelsPathHint": "Custom models path for validation (e.g. /v4/models)" + "modelsPathHint": "Custom models path for validation (e.g. /v4/models)", + "statusDeactivated": "Deactivated (Manual)", + "statusBanned": "Banned / Sandbox Violation", + "statusCreditsExhausted": "Insufficient Balance / Quota Exhausted" }, "settings": { "title": "Settings", @@ -1918,7 +1925,6 @@ "uploadFavicon": "Upload Favicon", "resetFavicon": "Reset Favicon", "faviconPreview": "Favicon Preview", - "promptCache": "Prompt Cache", "flushCache": "Flush Cache", "flushing": "Flushing…", "size": "Size", @@ -1940,8 +1946,8 @@ "thinkingBudgetDesc": "Control AI reasoning token usage across all requests", "passthrough": "Passthrough", "passthroughDesc": "No changes — client controls thinking budget", - "auto": "Auto", - "autoDesc": "Strip all thinking config — let provider decide", + "auto": "Auto Combo", + "autoDesc": "Self-healing smart routing pool (Performance optimized)", "custom": "Custom", "customDesc": "Set a fixed token budget for all requests", "adaptive": "Adaptive", @@ -2209,7 +2215,6 @@ "unsaved": "unsaved", "resetDefaults": "Reset Defaults", "saveProvider": "Save Provider", - "saving": "Saving...", "model": "Model", "models": "models", "moreProviders": "{count} more providers", @@ -2240,7 +2245,12 @@ "customPricingNote": "You can override default pricing for specific models. Custom overrides take priority over auto-detected pricing.", "editPricing": "Edit Pricing", "viewFullDetails": "View Full Details", - "themeCoral": "Coral" + "themeCoral": "Coral", + "adaptiveVolumeRouting": "Adaptive Volume Routing", + "adaptiveVolumeRoutingDesc": "Scale connections dynamically based on payload volume and throughput pressure.", + "days": "Days", + "lkgp": "LKGP Mode", + "lkgpDesc": "Last Known Good Provider (Predictable resilience)" }, "translator": { "title": "Translator", @@ -3102,6 +3112,9 @@ "model": "Model", "created": "Created", "expires": "Expires", - "actions": "Actions" + "actions": "Actions", + "deduplicatedRequests": "Deduplicated Requests", + "savedCalls": "Saved API Calls", + "totalProcessed": "Total Requests Processed" } } diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 352a253415..3c7c226358 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -994,7 +994,11 @@ } }, "templateFreeStack": "Free Stack ($0)", - "templateFreeStackDesc": "Round-robin across all free providers: Kiro (Claude), Qoder (5 models), Qwen (4 models), Gemini CLI. Zero cost, never stops coding." + "templateFreeStackDesc": "Round-robin across all free providers: Kiro (Claude), Qoder (5 models), Qwen (4 models), Gemini CLI. Zero cost, never stops coding.", + "auto": "Auto Combo", + "autoDesc": "Pool de roteamento inteligente (Otimizado)", + "lkgp": "Modo LKGP", + "lkgpDesc": "Último Provedor Bom Conhecido (Resiliência previsível)" }, "costs": { "title": "Custos", @@ -1776,7 +1780,10 @@ "chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)", "modelsPathLabel": "Models Endpoint Path", "modelsPathPlaceholder": "/models", - "modelsPathHint": "Custom models path for validation (e.g. /v4/models)" + "modelsPathHint": "Custom models path for validation (e.g. /v4/models)", + "statusDeactivated": "Desativado (Manual)", + "statusBanned": "Banido / Sandbox Violation", + "statusCreditsExhausted": "Saldo Insuficiente" }, "settings": { "title": "Configurações", @@ -1918,8 +1925,8 @@ "thinkingBudgetDesc": "Controle o uso de tokens de raciocínio da IA em todas as requisições", "passthrough": "Passagem Direta", "passthroughDesc": "Sem alterações — cliente controla orçamento de raciocínio", - "auto": "Automático", - "autoDesc": "Remove toda configuração de raciocínio — provedor decide", + "auto": "Auto Combo", + "autoDesc": "Pool de roteamento inteligente (Otimizado)", "custom": "Personalizado", "customDesc": "Define um orçamento fixo de tokens para todas as requisições", "adaptive": "Adaptativo", @@ -2217,7 +2224,31 @@ "customPricingNote": "Você pode sobrescrever preços padrão para modelos específicos. Sobrescritas personalizadas têm prioridade sobre preços detectados automaticamente.", "editPricing": "Editar Preços", "viewFullDetails": "Ver Detalhes Completos", - "themeCoral": "Coral" + "themeCoral": "Coral", + "adaptiveVolumeRouting": "Roteamento de Volume Adaptativo", + "adaptiveVolumeRoutingDesc": "Escala conexões dinamicamente com base no volume e pressão na taxa de transferência.", + "days": "Dias", + "lkgp": "Modo LKGP", + "lkgpDesc": "Último Provedor Bom Conhecido (Resiliência previsível)", + "memoryTitle": "Memória", + "memoryDesc": "Memória conversacional persistente entre sessões", + "memoryEnabled": "Ativar Memória", + "memoryEnabledDesc": "Quando ativado, injetará o contexto passado relevante de forma dinâmica.", + "maxTokens": "Tokens Máximos", + "retentionDays": "Retenção", + "recent": "Recente", + "recentDesc": "Janela cronológica", + "semantic": "Semântica", + "semanticDesc": "Busca vetorial", + "hybrid": "Híbrido", + "hybridDesc": "Recente + Semântico", + "skillsTitle": "Skills A2A", + "skillsDesc": "Ferramentas auto-executáveis", + "skillsEnabled": "Ativar Skills", + "skillsEnabledDesc": "Permite aos agentes executar consultas e gerar arquivos.", + "skillsComingSoon": "Marketplace em breve.", + "memorySkillsTitle": "Memória e Skills", + "memorySkillsDesc": "Contexto persistente e capacidades A2A" }, "translator": { "title": "Tradutor", @@ -3078,6 +3109,9 @@ "model": "Model", "created": "Created", "expires": "Expires", - "actions": "Actions" + "actions": "Actions", + "deduplicatedRequests": "Requisições Desduplicadas", + "savedCalls": "Chamadas API Poupadas", + "totalProcessed": "Total Processado" } } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 6c3fdbdeef..81c157c512 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -994,7 +994,11 @@ } }, "templateFreeStack": "Free Stack ($0)", - "templateFreeStackDesc": "Round-robin across all free providers: Kiro (Claude), Qoder (5 models), Qwen (4 models), Gemini CLI. Zero cost, never stops coding." + "templateFreeStackDesc": "Round-robin across all free providers: Kiro (Claude), Qoder (5 models), Qwen (4 models), Gemini CLI. Zero cost, never stops coding.", + "auto": "Auto Combo", + "autoDesc": "Pool de roteamento inteligente (Otimizado)", + "lkgp": "Modo LKGP", + "lkgpDesc": "Último Provedor Bom Conhecido (Resiliência previsível)" }, "costs": { "title": "Custos", @@ -1776,7 +1780,10 @@ "chatPathHint": "Custom chat path for providers with non-standard APIs (e.g. /v4/chat/completions)", "modelsPathLabel": "Models Endpoint Path", "modelsPathPlaceholder": "/models", - "modelsPathHint": "Custom models path for validation (e.g. /v4/models)" + "modelsPathHint": "Custom models path for validation (e.g. /v4/models)", + "statusDeactivated": "Desativado (Manual)", + "statusBanned": "Banido / Sandbox Violation", + "statusCreditsExhausted": "Saldo Insuficiente" }, "settings": { "title": "Configurações", @@ -1918,8 +1925,8 @@ "thinkingBudgetDesc": "Controle o uso do token de raciocínio de IA em todas as solicitações", "passthrough": "Passagem", "passthroughDesc": "Sem alterações – o cliente controla o orçamento pensado", - "auto": "Automático", - "autoDesc": "Remova todas as configurações de pensamento – deixe o provedor decidir", + "auto": "Auto Combo", + "autoDesc": "Pool de roteamento inteligente (Otimizado)", "custom": "Personalizado", "customDesc": "Defina um orçamento fixo de tokens para todas as solicitações", "adaptive": "Adaptativo", @@ -2217,7 +2224,31 @@ "customPricingNote": "Você pode substituir o preço padrão de modelos específicos. As substituições personalizadas têm prioridade sobre os preços detectados automaticamente.", "editPricing": "Editar preços", "viewFullDetails": "Ver detalhes completos", - "themeCoral": "Coral" + "themeCoral": "Coral", + "adaptiveVolumeRouting": "Roteamento de Volume Adaptativo", + "adaptiveVolumeRoutingDesc": "Escala conexões dinamicamente com base no volume e pressão na taxa de transferência.", + "days": "Dias", + "lkgp": "Modo LKGP", + "lkgpDesc": "Último Provedor Bom Conhecido (Resiliência previsível)", + "memoryTitle": "Memória", + "memoryDesc": "Memória conversacional persistente entre sessões", + "memoryEnabled": "Ativar Memória", + "memoryEnabledDesc": "Quando ativado, injetará o contexto passado relevante de forma dinâmica.", + "maxTokens": "Tokens Máximos", + "retentionDays": "Retenção", + "recent": "Recente", + "recentDesc": "Janela cronológica", + "semantic": "Semântica", + "semanticDesc": "Busca vetorial", + "hybrid": "Híbrido", + "hybridDesc": "Recente + Semântico", + "skillsTitle": "Skills A2A", + "skillsDesc": "Ferramentas auto-executáveis", + "skillsEnabled": "Ativar Skills", + "skillsEnabledDesc": "Permite aos agentes executar consultas e gerar arquivos.", + "skillsComingSoon": "Marketplace em breve.", + "memorySkillsTitle": "Memória e Skills", + "memorySkillsDesc": "Contexto persistente e capacidades A2A" }, "translator": { "title": "Tradutor", @@ -3078,6 +3109,9 @@ "model": "Model", "created": "Created", "expires": "Expires", - "actions": "Actions" + "actions": "Actions", + "deduplicatedRequests": "Requisições Desduplicadas", + "savedCalls": "Chamadas API Poupadas", + "totalProcessed": "Total Processado" } } diff --git a/src/lib/db/quotaSnapshots.ts b/src/lib/db/quotaSnapshots.ts index 49cfd8a582..c1d9bb6721 100644 --- a/src/lib/db/quotaSnapshots.ts +++ b/src/lib/db/quotaSnapshots.ts @@ -72,6 +72,7 @@ export function getAggregatedSnapshots(opts: { since: string; until?: string; bucketMinutes: number; + aggregateBy?: "provider" | "connection"; }): ProviderUtilizationPoint[] { const db = getDbInstance() as unknown as DbLike; const conditions: string[] = ["created_at >= ?"]; @@ -92,16 +93,23 @@ export function getAggregatedSnapshots(opts: { throw new Error("Invalid bucket size"); } + const groupFields = + opts.aggregateBy === "connection" + ? "bucket, provider, connection_id, window_key" + : "bucket, provider, window_key"; + const selectKey = + opts.aggregateBy === "connection" ? "provider || ':' || connection_id as provider" : "provider"; + const sql = ` SELECT datetime((strftime('%s', created_at) / ${bucketSeconds}) * ${bucketSeconds}, 'unixepoch') as bucket, - provider, + ${selectKey}, AVG(remaining_percentage) as remainingPct, MAX(is_exhausted) as isExhausted, window_key FROM quota_snapshots WHERE ${conditions.join(" AND ")} - GROUP BY bucket, provider, window_key + GROUP BY ${groupFields} ORDER BY bucket ASC `; diff --git a/src/shared/constants/routingStrategies.ts b/src/shared/constants/routingStrategies.ts index 988beb9820..3f91b02bd9 100644 --- a/src/shared/constants/routingStrategies.ts +++ b/src/shared/constants/routingStrategies.ts @@ -7,7 +7,9 @@ export type RoutingStrategyValue = | "random" | "least-used" | "cost-optimized" - | "strict-random"; + | "strict-random" + | "auto" + | "lkgp"; type RoutingStrategyOption = { value: RoutingStrategyValue; @@ -81,6 +83,20 @@ export const ROUTING_STRATEGIES: RoutingStrategyOption[] = [ settingsDescKey: "strictRandomDesc", icon: "casino", }, + { + value: "auto", + labelKey: "auto", + combosDescKey: "autoDesc", + settingsDescKey: "autoDesc", + icon: "auto_awesome", + }, + { + value: "lkgp", + labelKey: "lkgp", + combosDescKey: "lkgpDesc", + settingsDescKey: "lkgpDesc", + icon: "verified", + }, ]; export const SETTINGS_FALLBACK_STRATEGY_VALUES: RoutingStrategyValue[] = [ @@ -93,4 +109,6 @@ export const SETTINGS_FALLBACK_STRATEGY_VALUES: RoutingStrategyValue[] = [ "least-used", "cost-optimized", "strict-random", + "auto", + "lkgp", ]; diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index c13d25c4e9..80a5cf4b31 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -57,6 +57,7 @@ const PRIMARY_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [ { id: "analytics", href: "/dashboard/analytics", i18nKey: "analytics", icon: "analytics" }, { id: "limits", href: "/dashboard/limits", i18nKey: "limits", icon: "tune" }, { id: "cache", href: "/dashboard/cache", i18nKey: "cache", icon: "cached" }, + { id: "media", href: "/dashboard/media", i18nKey: "media", icon: "auto_awesome" }, ]; const CLI_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [ @@ -69,7 +70,6 @@ const CLI_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [ const DEBUG_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [ { id: "translator", href: "/dashboard/translator", i18nKey: "translator", icon: "translate" }, { id: "playground", href: "/dashboard/playground", i18nKey: "playground", icon: "science" }, - { id: "media", href: "/dashboard/media", i18nKey: "media", icon: "auto_awesome" }, { id: "search-tools", href: "/dashboard/search-tools", diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 330f7c165c..396ff4e8fc 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -82,8 +82,22 @@ const comboStrategySchema = z.enum([ "fill-first", // #729 schema fixes for combo edit/save "p2c", + "auto", + "lkgp", ]); +const scoringWeightsSchema = z + .object({ + quota: z.number().min(0).max(1), + health: z.number().min(0).max(1), + costInv: z.number().min(0).max(1), + latencyInv: z.number().min(0).max(1), + taskFit: z.number().min(0).max(1), + stability: z.number().min(0).max(1), + tierPriority: z.number().min(0).max(1).optional().default(0.05), + }) + .optional(); + const comboRuntimeConfigSchema = z .object({ strategy: comboStrategySchema.optional(), @@ -96,6 +110,13 @@ const comboRuntimeConfigSchema = z healthCheckTimeoutMs: z.coerce.number().int().min(100).max(30000).optional(), maxComboDepth: z.coerce.number().int().min(1).max(10).optional(), trackMetrics: z.boolean().optional(), + // Auto-Combo / LKGP Extensions + candidatePool: z.array(z.string().min(1)).optional(), + weights: scoringWeightsSchema.optional(), + modePack: z.string().max(100).optional(), + budgetCap: z.number().positive().optional(), + explorationRate: z.number().min(0).max(1).optional(), + routerStrategy: z.string().optional(), }) .strict(); @@ -117,18 +138,6 @@ export const createComboSchema = z.object({ // ──── Auto-Combo Schemas ──── -const scoringWeightsSchema = z - .object({ - quota: z.number().min(0).max(1), - health: z.number().min(0).max(1), - costInv: z.number().min(0).max(1), - latencyInv: z.number().min(0).max(1), - taskFit: z.number().min(0).max(1), - stability: z.number().min(0).max(1), - tierPriority: z.number().min(0).max(1).optional().default(0.05), - }) - .optional(); - export const createAutoComboSchema = z.object({ id: z.string().trim().min(1, "id is required").max(100), name: z.string().trim().min(1, "name is required").max(200),