-
{provider.name}
+
+ {provider.name}
+
+
{getStatusDisplay(connected, error, errorCode)}
{errorTime && • {errorTime}}
@@ -470,15 +498,24 @@ ProviderCard.propTypes = {
errorCode: PropTypes.string,
errorTime: PropTypes.string,
}).isRequired,
+ authType: PropTypes.string,
};
// API Key providers - use image with textIcon fallback (same as OAuth providers)
-function ApiKeyProviderCard({ providerId, provider, stats }) {
+function ApiKeyProviderCard({ providerId, provider, stats, authType }) {
const { connected, error, errorCode, errorTime } = stats;
const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
const isAnthropicCompatible = providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
const [imgError, setImgError] = useState(false);
+ const dotColors = {
+ free: "bg-green-500",
+ oauth: "bg-blue-500",
+ apikey: "bg-amber-500",
+ compatible: "bg-orange-500",
+ };
+ const dotLabels = { free: "Free", oauth: "OAuth", apikey: "API Key", compatible: "Compatible" };
+
// Determine icon path: OpenAI Compatible providers use specialized icons
const getIconPath = () => {
if (isCompatible) {
@@ -519,7 +556,13 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
)}
-
{provider.name}
+
+ {provider.name}
+
+
{getStatusDisplay(connected, error, errorCode)}
{isCompatible && (
@@ -560,6 +603,7 @@ ApiKeyProviderCard.propTypes = {
errorCode: PropTypes.string,
errorTime: PropTypes.string,
}).isRequired,
+ authType: PropTypes.string,
};
function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
diff --git a/src/app/api/provider-metrics/route.js b/src/app/api/provider-metrics/route.js
new file mode 100644
index 0000000000..6dcadc1ca3
--- /dev/null
+++ b/src/app/api/provider-metrics/route.js
@@ -0,0 +1,40 @@
+import { NextResponse } from "next/server";
+import { getDbInstance } from "@/lib/db/core.js";
+
+/**
+ * GET /api/providers/metrics — Aggregate per-provider stats from call_logs
+ * Returns: { metrics: { [provider]: { totalRequests, totalSuccesses, successRate, avgLatencyMs } } }
+ */
+export async function GET() {
+ try {
+ const db = getDbInstance();
+ const rows = db
+ .prepare(
+ `SELECT
+ provider,
+ COUNT(*) as totalRequests,
+ SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END) as totalSuccesses,
+ ROUND(AVG(duration)) as avgLatencyMs
+ FROM call_logs
+ WHERE provider IS NOT NULL AND provider != '-'
+ GROUP BY provider`
+ )
+ .all();
+
+ const metrics = {};
+ for (const row of rows) {
+ metrics[row.provider] = {
+ totalRequests: row.totalRequests,
+ totalSuccesses: row.totalSuccesses,
+ successRate:
+ row.totalRequests > 0 ? Math.round((row.totalSuccesses / row.totalRequests) * 100) : 0,
+ avgLatencyMs: row.avgLatencyMs || 0,
+ };
+ }
+
+ return NextResponse.json({ metrics });
+ } catch (error) {
+ console.error("[providers/metrics] Error:", error);
+ return NextResponse.json({ metrics: {} });
+ }
+}
diff --git a/src/app/api/providers/[id]/models/route.js b/src/app/api/providers/[id]/models/route.js
index e27e7e9858..c92bac5b9f 100644
--- a/src/app/api/providers/[id]/models/route.js
+++ b/src/app/api/providers/[id]/models/route.js
@@ -23,6 +23,13 @@ const STATIC_MODEL_PROVIDERS = {
{ id: "nanobanana-flash", name: "NanoBanana Flash (Gemini 2.5 Flash)" },
{ id: "nanobanana-pro", name: "NanoBanana Pro (Gemini 3 Pro)" },
],
+ perplexity: () => [
+ { id: "sonar", name: "Sonar (Fast Search)" },
+ { id: "sonar-pro", name: "Sonar Pro (Advanced Search)" },
+ { id: "sonar-reasoning", name: "Sonar Reasoning (CoT + Search)" },
+ { id: "sonar-reasoning-pro", name: "Sonar Reasoning Pro (Advanced CoT + Search)" },
+ { id: "sonar-deep-research", name: "Sonar Deep Research (Expert Analysis)" },
+ ],
};
// Provider models endpoints configuration
@@ -142,14 +149,7 @@ const PROVIDER_MODELS_CONFIG = {
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
- perplexity: {
- url: "https://api.perplexity.ai/models",
- method: "GET",
- headers: { "Content-Type": "application/json" },
- authHeader: "Authorization",
- authPrefix: "Bearer ",
- parseResponse: (data) => data.data || data.models || [],
- },
+
together: {
url: "https://api.together.xyz/v1/models",
method: "GET",