refactor(app): DRY pass — split large files, extract shared utils

S1: delete page.new.js (1724L abandoned) + remove dead getAntigravityProjectId
S2: split large files by natural seams
  - usage.js → usage/{github,google,claude,codex,kiro,minimax,misc,shared}.js
  - media-providers page → components/{Embedding,Tts,Generic,Stt}ExampleCard.js
  - EndpointPageClient → endpointConstants.js + endpointPing.js + components/
  - tokenRefresh.js → tokenRefresh/{dedup,providers}.js
  - ProviderLimits/index.js: 16 pure fn + 9 constants → utils.js
  - oauth/providers.js: 7 pure helpers → providerHelpers.js
S3: shared utils
  - getModelKind(m, fallback) → shared/constants/models.js (replaces 20× m.kind||m.type)
  - getStatusVariant → shared/utils/connectionStatus.js (dedup ConnectionRow/ConnectionsCard)
  - sseChunk → open-sse/utils/sse.js (dedup grok-web/perplexity-web)
  - fetchWithTimeout → usage/shared.js (replace 4× AbortController pattern in google.js)
fix: enableObservability2 field name in requestDetailsRepo
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-14 19:31:09 +07:00
co-authored by Cursor
parent d3f61aac2f
commit fbf973f2e7
44 changed files with 4211 additions and 5875 deletions
@@ -5,62 +5,21 @@ import PropTypes from "prop-types";
import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
// Locales that unlock wenyan (classical Chinese) caveman levels
const WENYAN_LOCALES = ["zh-CN", "zh-TW"];
const TUNNEL_BENEFITS = [
{ icon: "public", title: "Access Anywhere", desc: "Use your API from any network" },
{ icon: "group", title: "Share Endpoint", desc: "Share URL with team members" },
{ icon: "code", title: "Use in Cursor/Cline", desc: "Connect AI tools remotely" },
{ icon: "lock", title: "Encrypted", desc: "End-to-end TLS via Cloudflare" },
];
const TUNNEL_PING_INTERVAL_MS = 2000;
const TUNNEL_PING_MAX_MS = 300000;
const STATUS_POLL_FAST_MS = 5000;
const STATUS_POLL_SLOW_MS = 30000;
const REACHABLE_MISS_THRESHOLD = 5;
const CLIENT_PING_FAST_MS = 10000;
const CLIENT_PING_SLOW_MS = 60000;
const CLIENT_PING_TIMEOUT_MS = 5000;
// Browser-side health probe: must reach origin (not just CF/TS edge).
// cors mode → res.ok=false for 5xx (e.g. Cloudflare 530 when origin dead).
// /api/health route sets Access-Control-Allow-Origin: * → CORS works through tunnel.
async function clientPingUrl(url) {
if (!url) return false;
try {
const res = await fetch(`${url}/api/health`, {
mode: "cors",
cache: "no-store",
signal: AbortSignal.timeout(CLIENT_PING_TIMEOUT_MS),
});
return res.ok;
} catch { return false; }
}
// Race multiple URLs: resolve true as soon as any one passes ping.
async function clientPingAny(...urls) {
const checks = urls.filter(Boolean).map(clientPingUrl);
if (!checks.length) return false;
return new Promise((resolve) => {
let pending = checks.length;
checks.forEach((p) => p.then((ok) => {
if (ok) resolve(true);
else if (--pending === 0) resolve(false);
}));
});
}
const CAVEMAN_LEVELS = [
{ id: "lite", label: "Lite", desc: "Drop filler, keep grammar" },
{ id: "full", label: "Full", desc: "Drop articles, fragments OK" },
{ id: "ultra", label: "Ultra", desc: "Telegraphic, max compression" },
{ id: "wenyan-lite", label: "文 Lite", desc: "Classical Chinese, light compression", wenyan: true },
{ id: "wenyan", label: "文 Full", desc: "Maximum 文言文, 80-90% reduction", wenyan: true },
{ id: "wenyan-ultra", label: "文 Ultra", desc: "Extreme classical compression", wenyan: true },
];
import {
WENYAN_LOCALES,
TUNNEL_BENEFITS,
TUNNEL_PING_INTERVAL_MS,
TUNNEL_PING_MAX_MS,
STATUS_POLL_FAST_MS,
REACHABLE_MISS_THRESHOLD,
CLIENT_PING_FAST_MS,
CAVEMAN_LEVELS,
} from "./endpointConstants";
import { clientPingUrl, clientPingAny } from "./endpointPing";
import EndpointRow from "./components/EndpointRow";
import StatusAlert from "./components/StatusAlert";
import Tooltip from "./components/Tooltip";
import SecurityWarning from "./components/SecurityWarning";
export default function APIPageClient({ machineId }) {
const [keys, setKeys] = useState([]);
const [loading, setLoading] = useState(true);
@@ -1474,81 +1433,6 @@ export default function APIPageClient({ machineId }) {
);
}
/** Reusable endpoint row component */
function EndpointRow({ label, url, copyId, copied, onCopy, badge, actions }) {
return (
<div className="flex items-center gap-2">
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
(badge === "CF" || badge === "TS") ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
}`}>{label}</span>
<Input value={url} readOnly className="flex-1 font-mono text-sm" />
<button
onClick={() => onCopy(url, copyId)}
className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
>
<span className="material-symbols-outlined text-[18px]">{copied === copyId ? "check" : "content_copy"}</span>
</button>
{actions}
</div>
);
}
/** Reusable status alert */
function StatusAlert({ status, className = "" }) {
// Render URLs in message as clickable links
const renderMessage = (msg) => {
const parts = msg.split(/(https?:\/\/[^\s]+)/g);
return parts.map((part, i) =>
/^https?:\/\//.test(part)
? <a key={i} href={part} target="_blank" rel="noreferrer" className="underline font-medium">{part}</a>
: part
);
};
return (
<div className={`p-2 rounded text-sm ${className} ${status.type === "success" ? "bg-green-500/10 text-green-600 dark:text-green-400" :
status.type === "warning" ? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400" :
status.type === "info" ? "bg-blue-500/10 text-blue-600 dark:text-blue-400" :
"bg-red-500/10 text-red-600 dark:text-red-400"
}`}>
{renderMessage(status.message)}
</div>
);
}
/** Inline tooltip, Claude Code CLI style */
function Tooltip({ text }) {
return (
<span className="relative group inline-flex items-center">
<span className="material-symbols-outlined text-[14px] text-text-muted cursor-help">help</span>
<span className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 z-50 w-64 rounded bg-gray-900 dark:bg-gray-800 text-white text-xs px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg">
{text}
</span>
</span>
);
}
/** Security warning banner with optional action link */
function SecurityWarning({ message, action }) {
return (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-700 dark:text-amber-400">
<span className="material-symbols-outlined text-[16px] shrink-0 mt-0.5">warning</span>
<p className="text-xs flex-1">{message}</p>
{action && (
<a
href={action.href}
className="text-xs font-medium underline shrink-0 hover:opacity-80"
onClick={action.href.startsWith("#") ? (e) => {
e.preventDefault();
document.getElementById(action.href.slice(1))?.scrollIntoView({ behavior: "smooth" });
} : undefined}
>
{action.label}
</a>
)}
</div>
);
}
APIPageClient.propTypes = {
machineId: PropTypes.string.isRequired,
@@ -0,0 +1,22 @@
"use client";
import { Input } from "@/shared/components";
/** Reusable endpoint row component */
export default function EndpointRow({ label, url, copyId, copied, onCopy, badge, actions }) {
return (
<div className="flex items-center gap-2">
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
(badge === "CF" || badge === "TS") ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
}`}>{label}</span>
<Input value={url} readOnly className="flex-1 font-mono text-sm" />
<button
onClick={() => onCopy(url, copyId)}
className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
>
<span className="material-symbols-outlined text-[18px]">{copied === copyId ? "check" : "content_copy"}</span>
</button>
{actions}
</div>
);
}
@@ -0,0 +1,23 @@
"use client";
/** Security warning banner with optional action link */
export default function SecurityWarning({ message, action }) {
return (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-700 dark:text-amber-400">
<span className="material-symbols-outlined text-[16px] shrink-0 mt-0.5">warning</span>
<p className="text-xs flex-1">{message}</p>
{action && (
<a
href={action.href}
className="text-xs font-medium underline shrink-0 hover:opacity-80"
onClick={action.href.startsWith("#") ? (e) => {
e.preventDefault();
document.getElementById(action.href.slice(1))?.scrollIntoView({ behavior: "smooth" });
} : undefined}
>
{action.label}
</a>
)}
</div>
);
}
@@ -0,0 +1,23 @@
"use client";
/** Reusable status alert */
export default function StatusAlert({ status, className = "" }) {
const renderMessage = (msg) => {
const parts = msg.split(/(https?:\/\/[^\s]+)/g);
return parts.map((part, i) =>
/^https?:\/\//.test(part)
? <a key={i} href={part} target="_blank" rel="noreferrer" className="underline font-medium">{part}</a>
: part
);
};
return (
<div className={`p-2 rounded text-sm ${className} ${status.type === "success" ? "bg-green-500/10 text-green-600 dark:text-green-400" :
status.type === "warning" ? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400" :
status.type === "info" ? "bg-blue-500/10 text-blue-600 dark:text-blue-400" :
"bg-red-500/10 text-red-600 dark:text-red-400"
}`}>
{renderMessage(status.message)}
</div>
);
}
@@ -0,0 +1,13 @@
"use client";
/** Inline tooltip, Claude Code CLI style */
export default function Tooltip({ text }) {
return (
<span className="relative group inline-flex items-center">
<span className="material-symbols-outlined text-[14px] text-text-muted cursor-help">help</span>
<span className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 z-50 w-64 rounded bg-gray-900 dark:bg-gray-800 text-white text-xs px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg">
{text}
</span>
</span>
);
}
@@ -0,0 +1,26 @@
export const WENYAN_LOCALES = ["zh-CN", "zh-TW"];
export const TUNNEL_BENEFITS = [
{ icon: "public", title: "Access Anywhere", desc: "Use your API from any network" },
{ icon: "group", title: "Share Endpoint", desc: "Share URL with team members" },
{ icon: "code", title: "Use in Cursor/Cline", desc: "Connect AI tools remotely" },
{ icon: "lock", title: "Encrypted", desc: "End-to-end TLS via Cloudflare" },
];
export const TUNNEL_PING_INTERVAL_MS = 2000;
export const TUNNEL_PING_MAX_MS = 300000;
export const STATUS_POLL_FAST_MS = 5000;
export const STATUS_POLL_SLOW_MS = 30000;
export const REACHABLE_MISS_THRESHOLD = 5;
export const CLIENT_PING_FAST_MS = 10000;
export const CLIENT_PING_SLOW_MS = 60000;
export const CLIENT_PING_TIMEOUT_MS = 5000;
export const CAVEMAN_LEVELS = [
{ id: "lite", label: "Lite", desc: "Drop filler, keep grammar" },
{ id: "full", label: "Full", desc: "Drop articles, fragments OK" },
{ id: "ultra", label: "Ultra", desc: "Telegraphic, max compression" },
{ id: "wenyan-lite", label: "文 Lite", desc: "Classical Chinese, light compression", wenyan: true },
{ id: "wenyan", label: "文 Full", desc: "Maximum 文言文, 80-90% reduction", wenyan: true },
{ id: "wenyan-ultra", label: "文 Ultra", desc: "Extreme classical compression", wenyan: true },
];
@@ -0,0 +1,29 @@
import { CLIENT_PING_TIMEOUT_MS } from "./endpointConstants";
// Browser-side health probe: must reach origin (not just CF/TS edge).
// cors mode → res.ok=false for 5xx (e.g. Cloudflare 530 when origin dead).
// /api/health route sets Access-Control-Allow-Origin: * → CORS works through tunnel.
export async function clientPingUrl(url) {
if (!url) return false;
try {
const res = await fetch(`${url}/api/health`, {
mode: "cors",
cache: "no-store",
signal: AbortSignal.timeout(CLIENT_PING_TIMEOUT_MS),
});
return res.ok;
} catch { return false; }
}
// Race multiple URLs: resolve true as soon as any one passes ping.
export async function clientPingAny(...urls) {
const checks = urls.filter(Boolean).map(clientPingUrl);
if (!checks.length) return false;
return new Promise((resolve) => {
let pending = checks.length;
checks.forEach((p) => p.then((ok) => {
if (ok) resolve(true);
else if (--pending === 0) resolve(false);
}));
});
}
@@ -0,0 +1,254 @@
"use client";
import { useState, useEffect } from "react";
import { Card } from "@/shared/components";
import { getProviderAlias, isCustomEmbeddingProvider } from "@/shared/constants/providers";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { Row } from "./exampleShared";
const DEFAULT_RESPONSE_EXAMPLE = `{
"object": "list",
"data": [{
"object": "embedding",
"index": 0,
"embedding": [0.002301, -0.019212, 0.004815, -0.031249, ...]
}],
"model": "...",
"usage": { "prompt_tokens": 9, "total_tokens": 9 }
}`;
export function EmbeddingExampleCard({ providerId, customAlias }) {
const isCustom = isCustomEmbeddingProvider(providerId);
const providerAlias = isCustom ? (customAlias || providerId) : getProviderAlias(providerId);
const embeddingModels = isCustom ? [] : getModelsByProviderId(providerId).filter((m) => getModelKind(m) === "embedding");
const [selectedModel, setSelectedModel] = useState(embeddingModels[0]?.id ?? "");
const [input, setInput] = useState("The quick brown fox jumps over the lazy dog");
const [dimensions, setDimensions] = useState("");
const [apiKey, setApiKey] = useState("");
const [useTunnel, setUseTunnel] = useState(false);
const [localEndpoint, setLocalEndpoint] = useState("");
const [tunnelEndpoint, setTunnelEndpoint] = useState("");
const [result, setResult] = useState(null);
const [running, setRunning] = useState(false);
const [error, setError] = useState("");
const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard();
const { copied: copiedRes, copy: copyRes } = useCopyToClipboard();
useEffect(() => {
setLocalEndpoint(window.location.origin);
fetch("/api/keys")
.then((r) => r.json())
.then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); })
.catch(() => {});
fetch("/api/tunnel/status")
.then((r) => r.json())
.then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); })
.catch(() => {});
}, []);
const endpoint = useTunnel ? tunnelEndpoint : localEndpoint;
const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : "";
// Build request body — include dimensions only if user provided a positive number
const buildBody = () => {
const body = { model: modelFull, input: input.trim() };
const dim = Number(dimensions);
if (dimensions && Number.isFinite(dim) && dim > 0) body.dimensions = dim;
return body;
};
const curlSnippet = `curl -X POST ${endpoint}/v1/embeddings \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\
-d '${JSON.stringify(buildBody())}'`;
const handleRun = async () => {
if (!input.trim() || !modelFull) return;
setRunning(true);
setError("");
setResult(null);
const start = Date.now();
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch("/api/v1/embeddings", {
method: "POST",
headers,
body: JSON.stringify(buildBody()),
});
const latencyMs = Date.now() - start;
const data = await res.json();
if (!res.ok) { setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; }
setResult({ data, latencyMs });
} catch (e) {
setError(e.message || "Network error");
} finally {
setRunning(false);
}
};
// Compact embedding array: first 4 values + count
const formatResultJson = (data) => {
if (!data) return DEFAULT_RESPONSE_EXAMPLE;
const clone = JSON.parse(JSON.stringify(data));
(clone.data || []).forEach((item) => {
if (Array.isArray(item.embedding) && item.embedding.length > 4) {
item.embedding = [...item.embedding.slice(0, 4).map((v) => parseFloat(v.toFixed(6))), `... (${item.embedding.length} dims)`];
}
});
return JSON.stringify(clone, null, 2);
};
const resultJson = result ? JSON.stringify(result.data, null, 2) : "";
return (
<Card>
<h2 className="text-lg font-semibold mb-4">Example</h2>
<div className="flex flex-col gap-2.5">
{/* Model — text input for custom node, dropdown otherwise */}
<Row label="Model">
{isCustom ? (
<input
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
placeholder="e.g. voyage-3, embed-english-v3.0, text-embedding-3-small"
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
/>
) : (
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
{embeddingModels.map((m) => (
<option key={m.id} value={m.id}>{m.name || m.id}</option>
))}
</select>
)}
</Row>
{/* Endpoint */}
<Row label="Endpoint">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<input
value={endpoint}
onChange={(e) => useTunnel ? setTunnelEndpoint(e.target.value) : setLocalEndpoint(e.target.value)}
className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
placeholder="http://localhost:3000"
/>
{/* Tunnel toggle — only show if tunnel URL is available */}
{tunnelEndpoint && (
<button
onClick={() => setUseTunnel((v) => !v)}
title={useTunnel ? "Using tunnel" : "Using local"}
className={`flex items-center gap-1 text-xs px-2 py-1.5 rounded-lg border shrink-0 transition-colors ${
useTunnel ? "border-primary/40 bg-primary/10 text-primary" : "border-border text-text-muted hover:text-primary"
}`}
>
<span className="material-symbols-outlined text-[14px]">wifi_tethering</span>
Tunnel
</button>
)}
</div>
</Row>
{/* API Key */}
<Row label="API Key">
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-..."
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
/>
</Row>
{/* Input */}
<Row label="Input">
<div className="relative">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
{input && (
<button
type="button"
onClick={() => setInput("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
</Row>
{/* Dimensions (optional) — truncate embedding vector length */}
<Row label="Dimensions">
<input
type="number"
min="1"
value={dimensions}
onChange={(e) => setDimensions(e.target.value)}
placeholder="optional, e.g. 512, 1024 (leave empty for default)"
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
</Row>
{/* Curl + Run */}
<div className="mt-1">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<button
onClick={() => copyCurl(curlSnippet)}
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
{copiedCurl ? "Copied" : "Copy"}
</button>
<button
onClick={handleRun}
disabled={running || !input.trim() || !modelFull}
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
play_arrow
</span>
{running ? "Running..." : "Run"}
</button>
</div>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
</div>
{/* Error */}
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
{/* Response — default example or real result */}
<div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
Response {result && <span className="font-normal normal-case">&#9889; {result.latencyMs}ms</span>}
</span>
{result && (
<button
onClick={() => copyRes(resultJson)}
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
{copiedRes ? "Copied" : "Copy"}
</button>
)}
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-70">
{formatResultJson(result?.data)}
</pre>
</div>
</div>
</Card>
);
}
@@ -0,0 +1,539 @@
"use client";
import { useState, useEffect } from "react";
import { Card } from "@/shared/components";
import { MEDIA_PROVIDER_KINDS, getProviderAlias, resolveProviderId } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { Row, KIND_EXAMPLE_CONFIG } from "./exampleShared";
const CLOUDFLARE_TEST_IMAGE_URL = "https://pub-1fb693cb11cc46b2b2f656f51e015a2c.r2.dev/dog.png";
const CLOUDFLARE_TEST_MASK_URL = "https://pub-1fb693cb11cc46b2b2f656f51e015a2c.r2.dev/dog-mask.png";
function getImageEditDefaults(providerId, modelId) {
if (providerId !== "cloudflare-ai") return {};
if (modelId === "@cf/runwayml/stable-diffusion-v1-5-img2img") {
return { image: CLOUDFLARE_TEST_IMAGE_URL };
}
if (modelId === "@cf/runwayml/stable-diffusion-v1-5-inpainting") {
return { image: CLOUDFLARE_TEST_IMAGE_URL, mask_image: CLOUDFLARE_TEST_MASK_URL };
}
return {};
}
function toImagePreviewSrc(value) {
const trimmed = typeof value === "string" ? value.trim() : "";
if (!trimmed) return "";
if (/^(data:image\/|https?:\/\/)/i.test(trimmed)) return trimmed;
return `data:image/png;base64,${trimmed}`;
}
export function GenericExampleCard({ providerId, kind }) {
const providerAlias = getProviderAlias(providerId);
const resolvedId = resolveProviderId(providerAlias);
const safeProviderAlias = resolvedId === providerId ? providerAlias : providerId;
const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kind);
const exConfig = KIND_EXAMPLE_CONFIG[kind];
const safeExConfig = exConfig || {};
// Get models for this kind (e.g., type="image")
const kindModels = getModelsByProviderId(providerId).filter((m) => getModelKind(m) === kind);
// Kinds that need a model identifier in the request (image/video/music)
const KIND_NEEDS_MODEL = new Set(["image", "video", "music", "imageToText"]);
const needsModel = KIND_NEEDS_MODEL.has(kind);
const allowManualModel = needsModel && kindModels.length === 0;
const [selectedModel, setSelectedModel] = useState(kindModels[0]?.id ?? "");
const selectedModelObj = kindModels.find((m) => m.id === selectedModel);
const supportsEdit = !!selectedModelObj?.capabilities?.includes("edit");
const supportsMask = !!selectedModelObj?.capabilities?.includes("mask");
const [input, setInput] = useState(safeExConfig.defaultInput || "");
const [refImage, setRefImage] = useState("");
const [maskImage, setMaskImage] = useState("");
const [extraValues, setExtraValues] = useState(() =>
(safeExConfig.extraFields || []).reduce((acc, f) => { acc[f.key] = f.default ?? ""; return acc; }, {})
);
const [apiKey, setApiKey] = useState("");
const [useTunnel, setUseTunnel] = useState(false);
const [localEndpoint, setLocalEndpoint] = useState("");
const [tunnelEndpoint, setTunnelEndpoint] = useState("");
const [result, setResult] = useState(null);
const [progress, setProgress] = useState(null); // { stage, bytesReceived }
const [partialImage, setPartialImage] = useState(null);
const [imageOutputFormat, setImageOutputFormat] = useState("json"); // json | binary
const [binaryImageUrl, setBinaryImageUrl] = useState("");
const [running, setRunning] = useState(false);
const [error, setError] = useState("");
const [connections, setConnections] = useState([]);
const [pinnedConnectionId, setPinnedConnectionId] = useState("");
const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard();
const { copied: copiedRes, copy: copyRes } = useCopyToClipboard();
useEffect(() => {
setLocalEndpoint(window.location.origin);
fetch("/api/keys")
.then((r) => r.json())
.then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); })
.catch(() => {});
fetch("/api/tunnel/status")
.then((r) => r.json())
.then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); })
.catch(() => {});
// Load active connections of this provider for pinning
fetch("/api/providers/client")
.then((r) => r.json())
.then((d) => {
const conns = (d.connections || []).filter((c) => c.provider === providerId && c.isActive !== false);
setConnections(conns);
})
.catch(() => {});
}, [providerId]);
// Safe to early-return now that all hooks are declared
if (!kindConfig || !exConfig) return null;
const endpoint = useTunnel ? tunnelEndpoint : localEndpoint;
const apiPath = kindConfig.endpoint.path;
// webSearch/webFetch: use safeProviderAlias only. Other kinds: append model when present.
const modelFull = !needsModel
? safeProviderAlias
: (selectedModel ? `${safeProviderAlias}/${selectedModel}` : (allowManualModel ? "" : safeProviderAlias));
const imageEditDefaults = getImageEditDefaults(providerId, selectedModel);
const effectiveRefImage = refImage.trim() || imageEditDefaults.image || "";
const effectiveMaskImage = maskImage.trim() || imageEditDefaults.mask_image || "";
const refImagePreviewSrc = toImagePreviewSrc(effectiveRefImage);
const maskImagePreviewSrc = toImagePreviewSrc(effectiveMaskImage);
// Build request body with optional extra fields (only non-empty values)
const extraBodyFromFields = Object.entries(extraValues).reduce((acc, [k, v]) => {
if (v === "" || v === null || v === undefined) return acc;
if (typeof v === "number" && Number.isNaN(v)) return acc;
acc[k] = v;
return acc;
}, {});
const requestBody = {
model: modelFull,
[exConfig.bodyKey]: input,
...exConfig.extraBody,
...extraBodyFromFields,
...(supportsEdit && effectiveRefImage ? { image: effectiveRefImage } : {}),
...(supportsMask && effectiveMaskImage ? { mask_image: effectiveMaskImage } : {}),
};
// Streaming supported for codex image (Plus/Pro accounts) — disabled when binary output requested
const wantBinary = kind === "image" && imageOutputFormat === "binary";
const useStreaming = kind === "image" && providerId === "codex" && !wantBinary;
const apiPathWithQuery = `${apiPath}${wantBinary ? "?response_format=binary" : ""}`;
const headersPreview = `-H "Content-Type: application/json" \\\n -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}"${pinnedConnectionId ? ` \\\n -H "x-connection-id: ${pinnedConnectionId}"` : ""}${useStreaming ? ` \\\n -H "Accept: text/event-stream"` : ""}`;
const curlSnippet = `curl -X ${kindConfig.endpoint.method} ${endpoint}${apiPathWithQuery} \\
${headersPreview.replace(/\\\n /g, "\\\n ")} \\
-d '${JSON.stringify(requestBody)}'${wantBinary ? " \\\n --output image.png" : ""}`;
const handleRun = async () => {
if (!input.trim() || !modelFull) return;
setRunning(true);
setError("");
setResult(null);
setProgress(null);
setPartialImage(null);
if (binaryImageUrl) { try { URL.revokeObjectURL(binaryImageUrl); } catch {} setBinaryImageUrl(""); }
const start = Date.now();
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
if (pinnedConnectionId) headers["x-connection-id"] = pinnedConnectionId;
if (useStreaming) headers["Accept"] = "text/event-stream";
const body = { ...requestBody, model: modelFull };
const res = await fetch(`/api${apiPathWithQuery}`, {
method: kindConfig.endpoint.method,
headers,
body: JSON.stringify(body),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError(data?.error?.message || data?.error || `HTTP ${res.status}`);
return;
}
const ctype = res.headers.get("content-type") || "";
// Binary image response — convert to blob URL
if (ctype.startsWith("image/")) {
const blob = await res.blob();
const objUrl = URL.createObjectURL(blob);
setBinaryImageUrl(objUrl);
setResult({ data: { binary: true, mime: ctype, size: blob.size }, latencyMs: Date.now() - start });
return;
}
const isSse = ctype.includes("text/event-stream");
if (isSse && res.body) {
// Parse SSE: progress / partial_image / done / error
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
let finalData = null;
let streamErr = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let sep;
while ((sep = buf.indexOf("\n\n")) !== -1) {
const block = buf.slice(0, sep);
buf = buf.slice(sep + 2);
let evt = null, dataStr = "";
for (const line of block.split("\n")) {
if (line.startsWith("event:")) evt = line.slice(6).trim();
else if (line.startsWith("data:")) dataStr += line.slice(5).trim();
}
if (!evt) continue;
try {
const payload = dataStr ? JSON.parse(dataStr) : {};
if (evt === "progress") setProgress(payload);
else if (evt === "partial_image") setPartialImage(payload);
else if (evt === "done") finalData = payload;
else if (evt === "error") streamErr = payload?.message || "Stream error";
} catch {}
}
}
const latencyMs = Date.now() - start;
if (streamErr) { setError(streamErr); return; }
if (finalData) setResult({ data: finalData, latencyMs });
} else {
const data = await res.json();
const latencyMs = Date.now() - start;
setResult({ data, latencyMs });
}
} catch (e) {
setError(e.message || "Network error");
} finally {
setRunning(false);
}
};
// Mask large b64_json strings in JSON view to keep it readable
const maskB64 = (obj) => {
if (!obj || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(maskB64);
const out = {};
for (const [k, v] of Object.entries(obj)) {
out[k] = (k === "b64_json" && typeof v === "string" && v.length > 100)
? `<${v.length} chars base64>`
: maskB64(v);
}
return out;
};
const resultJson = result ? JSON.stringify(maskB64(result.data), null, 2) : "";
return (
<Card>
<h2 className="text-lg font-semibold mb-4">Example</h2>
<div className="flex flex-col gap-2.5">
{/* Model selector — dropdown if presets exist, else manual input for media kinds */}
{kindModels.length > 0 ? (
<Row label="Model">
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
{kindModels.map((m) => (
<option key={m.id} value={m.id}>{m.name || m.id}</option>
))}
</select>
</Row>
) : allowManualModel ? (
<Row label="Model">
<input
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
placeholder="Enter model id (provider-specific)"
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
/>
</Row>
) : null}
{/* Endpoint */}
<Row label="Endpoint">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<span className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
{endpoint}{apiPath}
</span>
{tunnelEndpoint && (
<button
onClick={() => setUseTunnel((v) => !v)}
title={useTunnel ? "Using tunnel" : "Using local"}
className={`flex items-center gap-1 text-xs px-2 py-1.5 rounded-lg border shrink-0 transition-colors ${
useTunnel ? "border-primary/40 bg-primary/10 text-primary" : "border-border text-text-muted hover:text-primary"
}`}
>
<span className="material-symbols-outlined text-[14px]">wifi_tethering</span>
Tunnel
</button>
)}
</div>
</Row>
{/* API Key */}
<Row label="API Key">
<span className="px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate block">
{apiKey ? `${apiKey.slice(0, 8)}${"\u2022".repeat(Math.min(20, apiKey.length - 8))}` : <span className="text-text-muted italic">No key configured</span>}
</span>
</Row>
{/* Connection picker - only show when 2+ connections (or any with email) */}
{connections.length > 0 && (
<Row label="Connection">
<select
value={pinnedConnectionId}
onChange={(e) => setPinnedConnectionId(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="">Auto (by priority)</option>
{connections.map((c) => {
const plan = c.providerSpecificData?.chatgptPlanType;
const label = c.email || c.name || c.id.slice(0, 8);
return (
<option key={c.id} value={c.id}>
{label}{plan ? ` [${plan}]` : ""}
</option>
);
})}
</select>
</Row>
)}
{/* Input */}
<Row label={exConfig.inputLabel}>
<div className="relative">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={exConfig.inputPlaceholder}
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
{input && (
<button
type="button"
onClick={() => setInput("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
</Row>
{/* Reference image (only for edit-capable image models) */}
{supportsEdit && (
<Row label="Ref Image (URL)">
<div className="flex flex-col gap-2">
<div className="relative">
<input
value={refImage}
onChange={(e) => setRefImage(e.target.value)}
placeholder={imageEditDefaults.image || "https://example.com/source.png"}
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
{refImage && (
<button
type="button"
onClick={() => setRefImage("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
{refImagePreviewSrc && (
<img
src={refImagePreviewSrc}
alt="Reference"
className="max-h-40 rounded-lg border border-border object-contain bg-sidebar"
onError={(e) => { e.currentTarget.style.display = "none"; }}
onLoad={(e) => { e.currentTarget.style.display = "block"; }}
/>
)}
</div>
</Row>
)}
{supportsMask && (
<Row label="Mask (URL)">
<div className="flex flex-col gap-2">
<div className="relative">
<input
value={maskImage}
onChange={(e) => setMaskImage(e.target.value)}
placeholder={imageEditDefaults.mask_image || "https://example.com/mask.png"}
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
{maskImage && (
<button
type="button"
onClick={() => setMaskImage("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
{maskImagePreviewSrc && (
<img
src={maskImagePreviewSrc}
alt="Mask"
className="max-h-40 rounded-lg border border-border object-contain bg-sidebar"
onError={(e) => { e.currentTarget.style.display = "none"; }}
onLoad={(e) => { e.currentTarget.style.display = "block"; }}
/>
)}
</div>
</Row>
)}
{/* Extra fields — for kinds without model concept (webSearch/webFetch), show all; otherwise filter by model.params */}
{(exConfig.extraFields || [])
.filter((f) => kindModels.length === 0 || (Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key)))
.map((f) => (
<Row key={f.key} label={f.label}>
{f.type === "select" ? (
<select
value={extraValues[f.key] ?? ""}
onChange={(e) => setExtraValues((s) => ({ ...s, [f.key]: e.target.value }))}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
{(f.options || []).map((opt) => (
<option key={opt} value={opt}>{opt === "" ? "(default)" : opt}</option>
))}
</select>
) : f.type === "text" ? (
<input
type="text"
value={extraValues[f.key] ?? ""}
placeholder={f.placeholder}
onChange={(e) => setExtraValues((s) => ({ ...s, [f.key]: e.target.value }))}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
) : (
<input
type="number"
value={extraValues[f.key] ?? ""}
min={f.min}
max={f.max}
onChange={(e) => setExtraValues((s) => ({ ...s, [f.key]: e.target.value === "" ? "" : Number(e.target.value) }))}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
)}
</Row>
))}
{/* Output Format toggle (image only) — last */}
{kind === "image" && (
<Row label="Output Format">
<select
value={imageOutputFormat}
onChange={(e) => setImageOutputFormat(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="json">JSON (Base64)</option>
<option value="binary">Binary File</option>
</select>
</Row>
)}
{/* Curl + Run */}
<div className="mt-1">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<button
onClick={() => copyCurl(curlSnippet)}
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
{copiedCurl ? "Copied" : "Copy"}
</button>
<button
onClick={handleRun}
disabled={running || !input.trim() || !modelFull}
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
play_arrow
</span>
{running ? "Running..." : "Run"}
</button>
</div>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
</div>
{/* Streaming progress */}
{(running || progress) && useStreaming && (
<div className="flex flex-col gap-2 px-3 py-2 rounded-lg bg-sidebar border border-border sm:flex-row sm:items-center sm:gap-3">
<span className="material-symbols-outlined text-[16px] text-primary" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
{running ? "progress_activity" : "check_circle"}
</span>
<span className="text-xs text-text-muted">
{progress?.stage || "starting"}
{!running && progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""}
</span>
</div>
)}
{/* Partial image preview (codex stream) */}
{partialImage?.b64_json && !result && (
<div>
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Partial preview</span>
<img
src={`data:image/png;base64,${partialImage.b64_json}`}
alt="Partial"
className="max-w-full rounded-lg border border-border mt-1.5 opacity-80"
/>
</div>
)}
{/* Error */}
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
{/* Response */}
<div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
Response {result && <span className="font-normal normal-case">&#9889; {result.latencyMs}ms</span>}
</span>
{result && (
<button
onClick={() => copyRes(resultJson)}
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
{copiedRes ? "Copied" : "Copy"}
</button>
)}
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-70">
{result ? resultJson : exConfig.defaultResponse}
</pre>
{kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && (
<div className="mt-2">
<div className="flex items-center justify-end mb-1.5">
<a
href={binaryImageUrl || (result?.data?.data?.[0]?.b64_json ? `data:image/png;base64,${result.data.data[0].b64_json}` : result?.data?.data?.[0]?.url || "")}
download="image.png"
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">download</span>
Download
</a>
</div>
<img
src={binaryImageUrl || (result?.data?.data?.[0]?.b64_json ? `data:image/png;base64,${result.data.data[0].b64_json}` : result?.data?.data?.[0]?.url)}
alt="Generated"
className="max-w-full rounded-lg border border-border"
/>
</div>
)}
</div>
</div>
</Card>
);
}
@@ -0,0 +1,290 @@
"use client";
import { useState, useEffect } from "react";
import { Card } from "@/shared/components";
import { getProviderAlias } from "@/shared/constants/providers";
import { getModelKind } from "@/shared/constants/models";
import { getModelsByProviderId } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { Row } from "./exampleShared";
export function SttExampleCard({ providerId }) {
const providerAlias = getProviderAlias(providerId);
const builtinSttModels = getModelsByProviderId(providerId).filter((m) => getModelKind(m) === "stt");
const [customSttModels, setCustomSttModels] = useState([]);
const sttModels = [...builtinSttModels, ...customSttModels];
const [selectedModel, setSelectedModel] = useState(builtinSttModels[0]?.id ?? "");
const selectedModelObj = sttModels.find((m) => m.id === selectedModel);
const allowedParams = Array.isArray(selectedModelObj?.params) ? selectedModelObj.params : [];
const [audioFile, setAudioFile] = useState(null);
const [language, setLanguage] = useState("");
const [prompt, setPrompt] = useState("");
const [responseFormat, setResponseFormat] = useState("json");
const [temperature, setTemperature] = useState("");
const [apiKey, setApiKey] = useState("");
const [useTunnel, setUseTunnel] = useState(false);
const [localEndpoint, setLocalEndpoint] = useState("");
const [tunnelEndpoint, setTunnelEndpoint] = useState("");
const [result, setResult] = useState(null);
const [latency, setLatency] = useState(null);
const [running, setRunning] = useState(false);
const [error, setError] = useState("");
const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard();
const { copied: copiedRes, copy: copyRes } = useCopyToClipboard();
useEffect(() => {
setLocalEndpoint(window.location.origin);
fetch("/api/keys")
.then((r) => r.json())
.then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); })
.catch(() => {});
fetch("/api/tunnel/status")
.then((r) => r.json())
.then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); })
.catch(() => {});
const loadCustom = () => {
fetch("/api/models/custom", { cache: "no-store" })
.then((r) => r.json())
.then((d) => {
const list = (d.models || []).filter((m) => getModelKind(m) === "stt" && m.providerAlias === providerAlias);
setCustomSttModels(list);
})
.catch(() => {});
};
loadCustom();
window.addEventListener("focus", loadCustom);
window.addEventListener("customModelChanged", loadCustom);
return () => {
window.removeEventListener("focus", loadCustom);
window.removeEventListener("customModelChanged", loadCustom);
};
}, [providerAlias]);
const endpoint = useTunnel ? tunnelEndpoint : localEndpoint;
const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : "";
const curlSnippet = `curl -X POST ${endpoint}/v1/audio/transcriptions \\
-H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\
-F "file=@${audioFile?.name || "audio.mp3"}" \\
-F "model=${modelFull}"${allowedParams.includes("language") && language ? ` \\\n -F "language=${language}"` : ""}${allowedParams.includes("response_format") ? ` \\\n -F "response_format=${responseFormat}"` : ""}${allowedParams.includes("temperature") && temperature ? ` \\\n -F "temperature=${temperature}"` : ""}${allowedParams.includes("prompt") && prompt ? ` \\\n -F "prompt=${prompt}"` : ""}`;
const handleRun = async () => {
if (!audioFile || !modelFull) return;
setRunning(true);
setError("");
setResult(null);
const start = Date.now();
try {
const fd = new FormData();
fd.append("file", audioFile);
fd.append("model", modelFull);
if (allowedParams.includes("language") && language) fd.append("language", language);
if (allowedParams.includes("response_format")) fd.append("response_format", responseFormat);
if (allowedParams.includes("temperature") && temperature) fd.append("temperature", temperature);
if (allowedParams.includes("prompt") && prompt) fd.append("prompt", prompt);
const headers = {};
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch("/api/v1/audio/transcriptions", { method: "POST", headers, body: fd });
setLatency(Date.now() - start);
const ct = res.headers.get("content-type") || "";
const data = ct.includes("application/json") ? await res.json() : await res.text();
if (!res.ok) {
setError(data?.error?.message || data?.error || data || `HTTP ${res.status}`);
return;
}
setResult(data);
} catch (e) {
setError(e.message || "Network error");
} finally {
setRunning(false);
}
};
const resultStr = typeof result === "string" ? result : (result ? JSON.stringify(result, null, 2) : `{\n "text": "Hello world..."\n}`);
return (
<Card>
<h2 className="text-lg font-semibold mb-4">Example</h2>
<div className="flex flex-col gap-2.5">
{/* Model */}
{sttModels.length > 0 ? (
<Row label="Model">
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
{sttModels.map((m) => (
<option key={m.id} value={m.id}>{m.name || m.id}</option>
))}
</select>
</Row>
) : (
<Row label="Model">
<input
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
placeholder="Enter model id"
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
/>
</Row>
)}
{/* Endpoint */}
<Row label="Endpoint">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<span className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
{endpoint}/v1/audio/transcriptions
</span>
{tunnelEndpoint && (
<button
onClick={() => setUseTunnel((v) => !v)}
title={useTunnel ? "Using tunnel" : "Using local"}
className={`flex items-center gap-1 text-xs px-2 py-1.5 rounded-lg border shrink-0 transition-colors ${
useTunnel ? "border-primary/40 bg-primary/10 text-primary" : "border-border text-text-muted hover:text-primary"
}`}
>
<span className="material-symbols-outlined text-[14px]">wifi_tethering</span>
Tunnel
</button>
)}
</div>
</Row>
{/* API Key */}
<Row label="API Key">
<span className="px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate block">
{apiKey ? `${apiKey.slice(0, 8)}${"\u2022".repeat(Math.min(20, apiKey.length - 8))}` : <span className="text-text-muted italic">No key configured</span>}
</span>
</Row>
{/* Audio file */}
<Row label="Audio File">
<div className="flex flex-col gap-2">
<input
type="file"
accept="audio/*,video/mp4,.m4a,.mp3,.wav,.ogg,.flac,.webm,.opus"
onChange={(e) => setAudioFile(e.target.files?.[0] || null)}
className="w-full text-xs text-text-muted file:mr-2 file:py-1 file:px-2.5 file:rounded-lg file:border file:border-border file:bg-background file:text-text-main hover:file:bg-sidebar file:cursor-pointer"
/>
{audioFile && (
<span className="text-xs text-text-muted font-mono">
{audioFile.name} · {(audioFile.size / 1024).toFixed(1)} KB
</span>
)}
</div>
</Row>
{/* Language (if model supports) */}
{allowedParams.includes("language") && (
<Row label="Language">
<input
value={language}
onChange={(e) => setLanguage(e.target.value)}
placeholder="e.g. en, vi, ja (auto-detect if empty)"
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
/>
</Row>
)}
{/* Prompt (if model supports) */}
{allowedParams.includes("prompt") && (
<Row label="Prompt">
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="optional context to improve accuracy"
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
</Row>
)}
{/* Temperature (if model supports) */}
{allowedParams.includes("temperature") && (
<Row label="Temperature">
<input
type="number"
step="0.1"
min="0"
max="1"
value={temperature}
onChange={(e) => setTemperature(e.target.value)}
placeholder="0 - 1 (default 0)"
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
</Row>
)}
{/* Response format (if model supports) */}
{allowedParams.includes("response_format") && (
<Row label="Response Format">
<select
value={responseFormat}
onChange={(e) => setResponseFormat(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="json">json</option>
<option value="text">text</option>
<option value="srt">srt</option>
<option value="verbose_json">verbose_json</option>
<option value="vtt">vtt</option>
</select>
</Row>
)}
{/* Curl + Run */}
<div className="mt-1">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<button
onClick={() => copyCurl(curlSnippet)}
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
{copiedCurl ? "Copied" : "Copy"}
</button>
<button
onClick={handleRun}
disabled={running || !audioFile || !modelFull}
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
play_arrow
</span>
{running ? "Transcribing..." : "Run"}
</button>
</div>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
</div>
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
{/* Response */}
<div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
Response {result && latency && <span className="font-normal normal-case">&#9889; {latency}ms</span>}
</span>
{result && (
<button
onClick={() => copyRes(resultStr)}
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
{copiedRes ? "Copied" : "Copy"}
</button>
)}
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-70">
{resultStr}
</pre>
</div>
</div>
</Card>
);
}
@@ -0,0 +1,566 @@
"use client";
import { useState, useEffect } from "react";
import { Card } from "@/shared/components";
import { AI_PROVIDERS, getProviderAlias } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders";
import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js";
import { GOOGLE_TTS_LANGUAGES } from "open-sse/config/googleTtsLanguages.js";
import { Row } from "./exampleShared";
const DEFAULT_TTS_RESPONSE_EXAMPLE = `// Audio will appear here after running.
// Example JSON response (response_format=json):
{
"format": "mp3",
"audio": "//NExAANaAIIAUAAANNNNNNNN..." // base64 encoded MP3
}`;
export function TtsExampleCard({ providerId }) {
const providerAlias = getProviderAlias(providerId);
const config = TTS_PROVIDER_CONFIG[providerId] || TTS_PROVIDER_CONFIG["edge-tts"];
// Voice state
const [selectedVoice, setSelectedVoice] = useState(config.defaultVoiceId || "");
const [selectedVoiceName, setSelectedVoiceName] = useState("");
const [voiceId, setVoiceId] = useState(config.defaultVoiceId || ""); // editable voice id (elevenlabs/config providers)
// Voices shown below Voice row after language selected
const [countryVoices, setCountryVoices] = useState([]);
const [selectedLang, setSelectedLang] = useState("");
const [selectedModel, setSelectedModel] = useState(() => {
const cfgModels = AI_PROVIDERS[providerId]?.ttsConfig?.models;
if (cfgModels?.length) return cfgModels[0].id;
if (config.hasModelSelector && config.modelKey) {
const models = getModelsByProviderId(config.modelKey);
return models?.[0]?.id || "";
}
return "";
});
// Form state
const [input, setInput] = useState("Hello, this is a text to speech test.");
const [apiKey, setApiKey] = useState("");
const [useTunnel, setUseTunnel] = useState(false);
const [localEndpoint, setLocalEndpoint] = useState("");
const [tunnelEndpoint, setTunnelEndpoint] = useState("");
const [responseFormat, setResponseFormat] = useState("mp3"); // mp3 | json
const [audioUrl, setAudioUrl] = useState("");
const [jsonResponse, setJsonResponse] = useState(null); // Store JSON response
const [running, setRunning] = useState(false);
const [error, setError] = useState("");
const [latency, setLatency] = useState(null);
const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard();
// Country picker modal state
const [modalOpen, setModalOpen] = useState(false);
const [languages, setLanguages] = useState([]);
const [modalLoading, setModalLoading] = useState(false);
const [modalSearch, setModalSearch] = useState("");
const [modalError, setModalError] = useState("");
const [byLang, setByLang] = useState({});
// Language hint (e.g. Gemini): controls the spoken language without affecting voice selection
const [languageHint, setLanguageHint] = useState("");
useEffect(() => {
setLocalEndpoint(window.location.origin);
fetch("/api/keys")
.then((r) => r.json())
.then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); })
.catch(() => {});
fetch("/api/tunnel/status")
.then((r) => r.json())
.then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); })
.catch(() => {});
// Pre-select default voice based on provider config
if (config.voiceSource === "hardcoded") {
const defaultModel = config.hasModelSelector && config.modelKey
? (getModelsByProviderId(config.modelKey)?.[0]?.id || "")
: "";
// Use per-model voices if available, else flat list
const voices = (config.voicesPerModel && defaultModel)
? (getTtsVoicesForModel(providerId, defaultModel) || [])
: getModelsByProviderId(config.voiceKey || providerId).filter((m) => getModelKind(m) === "tts");
if (voices.length) {
if (config.hasBrowseButton) {
// Google TTS: pre-select "en" (English) as default, show as single voice chip
const defaultVoice = voices.find((v) => v.id === "en") || voices[0];
setSelectedLang(defaultVoice.id);
setSelectedVoice(defaultVoice.id);
setSelectedVoiceName(defaultVoice.name);
setCountryVoices([{ id: defaultVoice.id, name: defaultVoice.name }]);
} else {
// OpenAI/OpenRouter: set voice chips directly (no language picker)
setCountryVoices(voices);
setSelectedVoice(voices[0].id);
setSelectedVoiceName(voices[0].name || voices[0].id);
}
}
}
// api-language (edge-tts, local-device, elevenlabs): NO default load, wait for user to pick language
// config (nvidia, hyperbolic, deepgram, huggingface, cartesia, playht, coqui, tortoise, inworld, qwen):
// use ttsConfig.models for model selector; voice is empty by default (backend uses provider default)
}, [providerId]);
// Update voices when model changes (voicesPerModel providers)
useEffect(() => {
if (!config.voicesPerModel || !selectedModel) return;
const voices = getTtsVoicesForModel(providerId, selectedModel) || [];
setCountryVoices(voices);
if (voices.length) {
setSelectedVoice(voices[0].id);
setSelectedVoiceName(voices[0].name || voices[0].id);
}
}, [selectedModel]);
// Open modal — load language list
const openModal = async () => {
setModalOpen(true);
setModalSearch("");
setModalError("");
if (languages.length) return; // already loaded
setModalLoading(true);
try {
if (config.voiceSource === "hardcoded") {
// Build languages/byLang from static providerModels data
const voiceKey = config.voiceKey || providerId;
const voices = getModelsByProviderId(voiceKey).filter((m) => getModelKind(m) === "tts");
const byLangMap = {};
for (const v of voices) {
if (!byLangMap[v.id]) byLangMap[v.id] = { code: v.id, name: v.name, voices: [{ id: v.id, name: v.name }] };
}
setByLang(byLangMap);
setLanguages(Object.values(byLangMap).sort((a, b) => a.name.localeCompare(b.name)));
} else {
// Use provider-specific apiEndpoint if available, else default to edge-tts voices API
const url = config.apiEndpoint
? config.apiEndpoint
: `/api/media-providers/tts/voices?provider=${providerId === "local-device" ? "local-device" : "edge-tts"}`;
const r = await fetch(url);
const d = await r.json();
if (d.error) { setModalError(d.error); return; }
setLanguages(d.languages || []);
setByLang(d.byLang || {});
}
} catch (e) {
setModalError(e.message);
} finally {
setModalLoading(false);
}
};
// Click language → close modal → show voices below
const handlePickLanguage = (lang) => {
setModalOpen(false);
setSelectedLang(lang.code);
const voices = byLang[lang.code]?.voices || [];
setCountryVoices(voices);
// Auto-select first voice
if (voices.length) {
setSelectedVoice(voices[0].id);
setSelectedVoiceName(voices[0].name);
if (config.hasVoiceIdInput) setVoiceId(voices[0].id);
}
};
const filteredLanguages = modalSearch
? languages.filter((c) =>
c.name.toLowerCase().includes(modalSearch.toLowerCase()) ||
c.code.toLowerCase().includes(modalSearch.toLowerCase())
)
: languages;
const endpoint = useTunnel ? tunnelEndpoint : localEndpoint;
// For ElevenLabs/config-driven: prefer manual voiceId (if any), else fall back to selectedVoice
const activeVoiceId = config.hasVoiceIdInput ? (voiceId || selectedVoice) : selectedVoice;
const modelFull = (() => {
if (config.hasModelSelector && selectedModel && activeVoiceId) return `${providerAlias}/${selectedModel}/${activeVoiceId}`;
if (config.hasModelSelector && selectedModel) return `${providerAlias}/${selectedModel}`;
if (activeVoiceId) return `${providerAlias}/${activeVoiceId}`;
return "";
})();
const ttsBody = (() => {
const b = { model: modelFull, input };
if (config.hasLanguageHint && languageHint) b.language = languageHint;
return b;
})();
const curlSnippet = `curl -X POST ${endpoint}/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""} \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\
-d '${JSON.stringify(ttsBody)}' \\
${responseFormat === "json" ? "" : "--output speech.mp3"}`;
const handleRun = async () => {
if (!input.trim() || !modelFull) return;
setRunning(true);
setError("");
setAudioUrl("");
setJsonResponse(null);
const start = Date.now();
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const url = `/api/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""}`;
const res = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({ ...ttsBody, input: input.trim() }),
});
setLatency(Date.now() - start);
if (!res.ok) {
const d = await res.json().catch(() => ({}));
setError(d?.error?.message || d?.error || `HTTP ${res.status}`);
return;
}
if (responseFormat === "json") {
const data = await res.json();
setJsonResponse(data); // Store full JSON response
const audioBlob = await fetch(`data:audio/mp3;base64,${data.audio}`).then(r => r.blob());
setAudioUrl(URL.createObjectURL(audioBlob));
} else {
const blob = await res.blob();
setAudioUrl(URL.createObjectURL(blob));
}
} catch (e) {
setError(e.message || "Network error");
} finally {
setRunning(false);
}
};
return (
<>
<Card>
<h2 className="text-lg font-semibold mb-4">Example</h2>
<div className="flex flex-col gap-2.5">
{/* Endpoint + API Key as read-only text */}
<Row label="Endpoint">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<span className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
{endpoint}/v1/audio/speech
</span>
{tunnelEndpoint && (
<button
onClick={() => setUseTunnel((v) => !v)}
title={useTunnel ? "Using tunnel" : "Using local"}
className={`flex items-center gap-1 text-xs px-2 py-1.5 rounded-lg border shrink-0 transition-colors ${
useTunnel ? "border-primary/40 bg-primary/10 text-primary" : "border-border text-text-muted hover:text-primary"
}`}
>
<span className="material-symbols-outlined text-[14px]">wifi_tethering</span>
Tunnel
</button>
)}
</div>
</Row>
<Row label="API Key">
<span className="px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate block">
{apiKey ? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}` : <span className="text-text-muted italic">No key configured</span>}
</span>
</Row>
{/* Model selector — prefer PROVIDER_MODELS[kind=tts], else providerModels via modelKey */}
{config.hasModelSelector && (config.modelKey || getModelsByProviderId(providerId).some(m => getModelKind(m) === "tts")) && (
<Row label="Model">
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
{(() => {
const ttsModels = getModelsByProviderId(providerId).filter(m => getModelKind(m) === "tts");
return (ttsModels.length ? ttsModels : getModelsByProviderId(config.modelKey) || []).map((m) => (
<option key={m.id} value={m.id}>{m.name || m.id}</option>
));
})()}
</select>
</Row>
)}
{/* Language hint dropdown (Gemini) — sends body.language to guide pronunciation */}
{config.hasLanguageHint && (
<Row label="Language">
<select
value={languageHint}
onChange={(e) => setLanguageHint(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="">Auto-detect</option>
{GOOGLE_TTS_LANGUAGES.map((l) => (
<option key={l.id} value={l.name}>{l.name}</option>
))}
</select>
</Row>
)}
{/* Language row + Browse button (edge-tts, local-device, elevenlabs) */}
{config.hasBrowseButton && (
<Row label="Language">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<button
onClick={openModal}
className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background font-mono truncate text-left hover:border-primary/40 transition-colors"
>
{selectedLang
? <span className="text-text-main">{languages.find((l) => l.code === selectedLang)?.name || selectedLang}</span>
: <span className="text-text-muted">No language selected</span>}
</button>
<button
onClick={openModal}
className="flex w-full items-center justify-center gap-1 text-xs px-2.5 py-1.5 rounded-lg border border-border text-text-muted hover:text-primary hover:border-primary/40 transition-colors sm:w-auto sm:shrink-0"
>
<span className="material-symbols-outlined text-[14px]">language</span>
Select language
</button>
</div>
</Row>
)}
{/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs) */}
{countryVoices.length > 0 && (
<Row label="Voice">
<div className="flex flex-wrap gap-1.5">
{countryVoices.map((v) => (
<button
key={v.id}
onClick={() => {
setSelectedVoice(v.id);
setSelectedVoiceName(v.name);
if (config.hasVoiceIdInput) setVoiceId(v.id);
}}
className={`px-2.5 py-1 rounded-full text-xs border transition-colors ${
selectedVoice === v.id
? "bg-primary/15 border-primary/40 text-primary font-medium"
: "border-border text-text-muted hover:text-primary hover:border-primary/40"
}`}
>
{v.name}{v.gender ? ` · ${v.gender[0].toUpperCase()}` : ""}
{v.free_users_allowed === true && (
<span className="ml-1.5 px-1 py-0.5 text-[9px] font-semibold rounded bg-green-500/15 text-green-600 border border-green-500/20">Free</span>
)}
{v.free_users_allowed === false && (
<span className="ml-1.5 px-1 py-0.5 text-[9px] font-semibold rounded bg-amber-500/15 text-amber-600 border border-amber-500/20">Paid</span>
)}
</button>
))}
</div>
</Row>
)}
{/* Voice ID input (ElevenLabs) — manual entry or auto-fill from chip */}
{config.hasVoiceIdInput && (
<Row label="Voice ID">
<div className="flex flex-col gap-1">
<div className="relative">
<input
value={voiceId}
onChange={(e) => {
setVoiceId(e.target.value);
setSelectedVoice(e.target.value);
}}
placeholder="e.g. CwhRBWXzGAHq8TQ4Fs17"
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
/>
{voiceId && (
<button
type="button"
onClick={() => { setVoiceId(""); setSelectedVoice(""); }}
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
</div>
</Row>
)}
{/* Google TTS: Language dropdown */}
{config.hasLanguageDropdown && (
<Row label="Language">
<select
value={selectedVoice}
onChange={(e) => {
const m = getModelsByProviderId(providerId).filter((m) => getModelKind(m) === "tts").find((m) => m.id === e.target.value);
setSelectedVoice(e.target.value);
setSelectedVoiceName(m?.name || e.target.value);
}}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
{getModelsByProviderId(providerId).filter((m) => getModelKind(m) === "tts").map((m) => (
<option key={m.id} value={m.id}>{m.name || m.id}</option>
))}
</select>
</Row>
)}
{/* Input */}
<Row label="Input">
<div className="relative">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
{input && (
<button
type="button"
onClick={() => setInput("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
</Row>
{/* Output Format */}
<Row label="Output Format">
<select
value={responseFormat}
onChange={(e) => setResponseFormat(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
>
<option value="mp3">MP3 (Binary)</option>
<option value="json">JSON (Base64)</option>
</select>
</Row>
{/* Curl + Run */}
<div className="mt-1">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<button
onClick={() => copyCurl(curlSnippet)}
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
{copiedCurl ? "Copied" : "Copy"}
</button>
<button
onClick={handleRun}
disabled={running || !input.trim() || !modelFull}
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
play_arrow
</span>
{running ? "Generating..." : "Run"}
</button>
</div>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
</div>
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
{/* Audio player */}
{audioUrl ? (
<div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
Response {latency && <span className="font-normal normal-case">&#9889; {latency}ms</span>}
</span>
<a href={audioUrl} download="speech.mp3" className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors">
<span className="material-symbols-outlined text-[14px]">download</span>
Download
</a>
</div>
<audio controls src={audioUrl} className="w-full" />
{/* JSON Response (if format is json) */}
{jsonResponse && (
<div className="mt-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">JSON Response</span>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify({
format: jsonResponse.format,
audio: jsonResponse.audio ? `${jsonResponse.audio.substring(0, 100)}...` : ""
}, null, 2)}
</pre>
</div>
)}
</div>
) : (
<div>
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Response</span>
<pre className="mt-1.5 bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-50">{DEFAULT_TTS_RESPONSE_EXAMPLE}</pre>
</div>
)}
</div>
</Card>
{/* Country Picker Modal */}
{modalOpen && (
<div
className="fixed inset-0 z-50 flex items-end justify-center sm:items-center"
style={{ backgroundColor: "rgba(0,0,0,0.6)", backdropFilter: "blur(2px)" }}
onClick={() => setModalOpen(false)}
>
<div
className="border border-border rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col max-h-[80vh]"
style={{ backgroundColor: "var(--color-bg)", isolation: "isolate" }}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0 rounded-t-xl">
<h3 className="text-sm font-semibold">Select Language</h3>
<button onClick={() => setModalOpen(false)} className="text-text-muted hover:text-primary transition-colors">
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
{/* Search */}
<div className="px-4 py-2.5 border-b border-border shrink-0">
<input
autoFocus
value={modalSearch}
onChange={(e) => setModalSearch(e.target.value)}
placeholder="Search language..."
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
</div>
{/* Language list */}
<div className="overflow-y-auto flex-1 p-2">
{modalError && <p className="text-xs text-red-500 px-2 py-1">{modalError}</p>}
{modalLoading ? (
<p className="text-xs text-text-muted px-2 py-3">Loading...</p>
) : (
<div className="flex flex-col gap-0.5">
{filteredLanguages.map((c) => (
<button
key={c.code}
onClick={() => handlePickLanguage(c)}
className={`flex items-center justify-between w-full px-3 py-2 rounded-lg text-left hover:bg-sidebar transition-colors ${
selectedLang === c.code ? "bg-primary/10 text-primary" : ""
}`}
>
<span className="text-sm">{c.name}</span>
<div className="flex items-center gap-2 shrink-0">
<span className="text-xs text-text-muted">{c.voices.length} voices</span>
{selectedLang === c.code && (
<span className="material-symbols-outlined text-[16px] text-primary">check</span>
)}
</div>
</button>
))}
{filteredLanguages.length === 0 && (
<p className="text-xs text-text-muted px-2 py-3">No languages found.</p>
)}
</div>
)}
</div>
</div>
</div>
)}
</>
);
}
@@ -0,0 +1,76 @@
"use client";
export function Row({ label, children }) {
return (
<div className="flex min-w-0 flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-3">
<span className="w-full text-xs font-medium text-text-muted sm:w-20 sm:shrink-0">{label}</span>
<div className="w-full min-w-0 flex-1">{children}</div>
</div>
);
}
export const KIND_EXAMPLE_CONFIG = {
webSearch: {
inputLabel: "Query",
inputPlaceholder: "What is the latest news about AI?",
defaultInput: "What is the latest news about AI?",
bodyKey: "query",
defaultResponse: `{\n "results": [\n { "title": "...", "url": "...", "snippet": "..." }\n ]\n}`,
extraFields: [
{ key: "search_type", label: "Type", type: "select", default: "web", options: ["web", "news"] },
{ key: "max_results", label: "Max results", type: "number", default: 5, min: 1, max: 100 },
{ key: "country", label: "Country", type: "text", default: "" },
{ key: "language", label: "Language", type: "text", default: "" },
],
},
webFetch: {
inputLabel: "URL",
inputPlaceholder: "https://example.com",
defaultInput: "https://example.com",
bodyKey: "url",
defaultResponse: `{\n "content": "...",\n "title": "...",\n "url": "..."\n}`,
extraFields: [
{ key: "format", label: "Format", type: "select", default: "markdown", options: ["markdown", "text", "html"] },
{ key: "max_characters", label: "Max chars", type: "number", default: 0, min: 0 },
],
},
image: {
inputLabel: "Prompt",
inputPlaceholder: "A cute cat wearing a hat",
defaultInput: "A cute cat wearing a hat",
bodyKey: "prompt",
defaultResponse: `{\n "data": [\n { "url": "...", "b64_json": "..." }\n ]\n}`,
extraFields: [
{ key: "n", label: "n", type: "number", default: 1, min: 1, max: 4 },
{ key: "size", label: "Size", type: "select", default: "auto", options: ["auto", "1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"] },
{ key: "quality", label: "Quality", type: "select", default: "auto", options: ["auto", "low", "medium", "high", "standard", "hd"] },
{ key: "background", label: "Background", type: "select", default: "auto", options: ["auto", "transparent", "opaque"] },
{ key: "style", label: "Style", type: "select", default: "", options: ["", "vivid", "natural"] },
{ key: "response_format", label: "Format", type: "select", default: "", options: ["", "url", "b64_json"] },
{ key: "image_detail", label: "Image Detail", type: "select", default: "high", options: ["auto", "low", "high", "original"] },
{ key: "output_format", label: "Codec", type: "select", default: "png", options: ["png", "jpeg", "webp"] },
],
},
imageToText: {
inputLabel: "Image URL",
inputPlaceholder: "https://example.com/image.png",
defaultInput: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg",
bodyKey: "url",
extraBody: { prompt: "Describe this image in detail" },
defaultResponse: `{\n "text": "A cat sitting on a windowsill...",\n "model": "..."\n}`,
},
video: {
inputLabel: "Prompt",
inputPlaceholder: "A serene lake at sunset",
defaultInput: "A serene lake at sunset",
bodyKey: "prompt",
defaultResponse: `{\n "data": [\n { "url": "..." }\n ]\n}`,
},
music: {
inputLabel: "Prompt",
inputPlaceholder: "A calm piano melody",
defaultInput: "A calm piano melody",
bodyKey: "prompt",
defaultResponse: `{\n "data": [\n { "url": "...", "format": "mp3" }\n ]\n}`,
},
};
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { getStatusVariant as getConnectionStatusVariant } from "@/shared/utils/connectionStatus";
import PropTypes from "prop-types";
import { Badge, Toggle } from "@/shared/components";
import CooldownTimer from "./CooldownTimer";
@@ -107,12 +108,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
? "active" // Cooldown expired u2192 treat as active
: connection.testStatus;
const getStatusVariant = () => {
if (connection.isActive === false) return "default";
if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
return "default";
};
const getStatusVariant = () => getConnectionStatusVariant(connection.isActive, effectiveStatus);
const getOneByOneVariant = () => {
if (!oneByOneStatus) return "default";
@@ -6,7 +6,7 @@ import Link from "next/link";
import Image from "next/image";
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal } from "@/shared/components";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { translate } from "@/i18n/runtime";
import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
@@ -914,7 +914,7 @@ export default function ProviderDetailPage() {
const allModels = [
...models,
...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
].filter((m) => { const k = m.kind || m.type; return !k || k === "llm"; });
].filter((m) => { const k = getModelKind(m); return !k || k === "llm"; });
const disabledSet = new Set(disabledModelIds);
const displayModels = allModels.filter((m) => !disabledSet.has(m.id));
const disabledDisplayModels = allModels.filter((m) => disabledSet.has(m.id));
@@ -1449,7 +1449,7 @@ export default function ProviderDetailPage() {
const allIds = [
...models,
...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
].filter((m) => { const k = m.kind || m.type; return !k || k === "llm"; }).map((m) => m.id);
].filter((m) => { const k = getModelKind(m); return !k || k === "llm"; }).map((m) => m.id);
const activeIds = allIds.filter((id) => !disabledModelIds.includes(id));
return (
<div className="flex gap-2">
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { getStatusVariant as getConnectionStatusVariant } from "@/shared/utils/connectionStatus";
import PropTypes from "prop-types";
import { Card, Badge, Button, Modal, Select, Toggle, EditConnectionModal, ConfirmModal } from "@/shared/components";
@@ -86,12 +87,7 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
const effectiveStatus = connection.testStatus === "unavailable" && !isCooldown ? "active" : connection.testStatus;
const getStatusVariant = () => {
if (connection.isActive === false) return "default";
if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
return "default";
};
const getStatusVariant = () => getConnectionStatusVariant(connection.isActive, effectiveStatus);
const displayName = isOAuth
? connection.name || connection.email || connection.displayName || "OAuth Account"
@@ -3,7 +3,7 @@
import { useState, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import { Card, Button, Modal } from "@/shared/components";
import { getModelsByProviderId } from "@/shared/constants/models";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { getProviderAlias } from "@/shared/constants/providers";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
@@ -206,14 +206,14 @@ export default function ModelsCard({ providerId, kindFilter, providerAliasOverri
const builtInModels = kindFilter
? allBuiltIn.filter((m) => {
if (m.kinds) return m.kinds.includes(kindFilter);
return (m.kind || m.type || "llm") === kindFilter;
return getModelKind(m, "llm") === kindFilter;
})
: allBuiltIn;
// Custom models for this provider + kind, dedupe vs built-in
const myCustomModels = customModels.filter(
(m) => m.providerAlias === providerAlias
&& (m.kind || m.type || "llm") === effectiveType
&& getModelKind(m, "llm") === effectiveType
&& !builtInModels.some((b) => b.id === m.id)
);
@@ -4,222 +4,39 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import ProviderIcon from "@/shared/components/ProviderIcon";
import QuotaTable from "./QuotaTable";
import Toggle from "@/shared/components/Toggle";
import { parseQuotaData, calculatePercentage } from "./utils";
import {
parseQuotaData,
calculatePercentage,
getConnectionLabel,
getConnectionQuotaRemaining,
sortVisibleConnections,
buildLoadingState,
filterQuotaStateByConnections,
getConnectionsEmptyMessage,
getPageSizeLabel,
getConnectionsPaginationSummary,
getSafePagination,
getSafeTotals,
shouldResetPage,
getPaginationPageValue,
getProviderOptions,
reconcileConnectionsPage,
getQuotaCache,
setQuotaCache,
QUOTA_CACHE_KEY,
REFRESH_INTERVAL_MS,
DEPLETED_QUOTA_THRESHOLD,
AUTO_REFRESH_STORAGE_KEY,
CONNECTIONS_PAGE_SIZE,
ACCOUNT_PAGE_SIZE_OPTIONS,
ACCOUNT_PAGE_SIZE_MAX,
ACCOUNT_FILTER_OPTIONS,
QUOTA_SORT_OPTIONS,
} from "./utils";
import Card from "@/shared/components/Card";
import { EditConnectionModal } from "@/shared/components";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
function getConnectionLabel(connection) {
const isEmail = (value) =>
typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
if (isEmail(connection.email)) return connection.email;
if (isEmail(connection.name)) return connection.name;
return connection.name;
}
function getConnectionQuotaRemaining(connection, quotaData) {
const quota = quotaData[connection.id]?.quotas?.[0];
if (!quota) return Number.POSITIVE_INFINITY;
if (typeof quota.remaining === "number") return quota.remaining;
return Number.POSITIVE_INFINITY;
}
function sortVisibleConnections(
connections,
quotaData,
expiringFirst,
providerFilter,
quotaSortMode,
) {
if (providerFilter === "codex" && quotaSortMode !== "default") {
return [...connections].sort((a, b) => {
const remainingA = getConnectionQuotaRemaining(a, quotaData);
const remainingB = getConnectionQuotaRemaining(b, quotaData);
const remainingDiff =
quotaSortMode === "remaining-asc"
? remainingA - remainingB
: remainingB - remainingA;
if (remainingDiff !== 0) return remainingDiff;
return (getConnectionLabel(a) || "").localeCompare(
getConnectionLabel(b) || "",
);
});
}
if (!expiringFirst) return connections;
const getEarliestResetTime = (connection) => {
const resetTimes = (quotaData[connection.id]?.quotas || [])
.map((quota) =>
quota.resetAt
? new Date(quota.resetAt).getTime()
: Number.POSITIVE_INFINITY,
)
.filter((time) => Number.isFinite(time));
return resetTimes.length > 0
? Math.min(...resetTimes)
: Number.POSITIVE_INFINITY;
};
return [...connections].sort((a, b) => {
const expiryDiff = getEarliestResetTime(a) - getEarliestResetTime(b);
if (expiryDiff !== 0) return expiryDiff;
return (
(a.provider || "").localeCompare(b.provider || "") ||
(getConnectionLabel(a) || "").localeCompare(getConnectionLabel(b) || "")
);
});
}
function buildLoadingState(connections) {
const nextLoadingState = {};
connections.forEach((connection) => {
nextLoadingState[connection.id] = true;
});
return nextLoadingState;
}
function filterQuotaStateByConnections(state, connections) {
const visibleIds = new Set(connections.map((connection) => connection.id));
return Object.fromEntries(
Object.entries(state).filter(([id]) => visibleIds.has(id)),
);
}
function getConnectionsPageRange(pagination) {
if (!pagination.total) {
return { start: 0, end: 0 };
}
const start = (pagination.page - 1) * pagination.pageSize + 1;
const end = Math.min(pagination.page * pagination.pageSize, pagination.total);
return { start, end };
}
function getConnectionsEmptyMessage(totals, providerFilter, accountFilter) {
if (!totals.eligibleConnections) {
return {
icon: "cloud_off",
title: "No Providers Connected",
description:
"Connect to providers with OAuth to track your API quota limits and usage.",
};
}
if (!totals.providerFilteredConnections) {
return {
icon: "filter_alt_off",
title: "No Accounts Match Current Filters",
description:
providerFilter === "all"
? "Try changing the account status filter to see more quota trackers."
: `No ${accountFilter === "inactive" ? "turned off" : accountFilter === "active" ? "active" : "matching"} accounts found for ${providerFilter}.`,
};
}
return {
icon: "filter_alt_off",
title: "No Accounts On This Page",
description:
"Try moving to another page or refreshing the current filters.",
};
}
function sortRequestFromExpiringFirst(expiringFirst) {
return expiringFirst ? "expiring" : "priority";
}
function getPageSizeLabel(pageSize, isCustomPageSize) {
return isCustomPageSize ? `Custom: ${pageSize} / page` : `${pageSize} / page`;
}
function getConnectionsPaginationSummary(pagination) {
const { start, end } = getConnectionsPageRange(pagination);
return `Showing ${start}-${end} of ${pagination.total}`;
}
function getSafePagination(pagination, fallbackPageSize) {
return (
pagination || {
page: 1,
pageSize: fallbackPageSize,
total: 0,
totalPages: 1,
}
);
}
function getSafeTotals(totals, fallbackTotal = 0) {
return (
totals || {
eligibleConnections: fallbackTotal,
providerFilteredConnections: fallbackTotal,
}
);
}
function shouldResetPage(previousValue, nextValue) {
return previousValue !== nextValue;
}
function getPaginationPageValue(dataPagination, fallbackPage) {
return dataPagination?.page || fallbackPage;
}
function getProviderOptions(dataProviderOptions) {
return dataProviderOptions || [];
}
async function reconcileConnectionsPage(fetchConnections, targetPage) {
const nextConnections = await fetchConnections(targetPage);
return nextConnections;
}
const QUOTA_CACHE_KEY = "quotaCacheData";
function getQuotaCache() {
if (typeof window === "undefined") return {};
try {
const cached = window.localStorage.getItem(QUOTA_CACHE_KEY);
return cached ? JSON.parse(cached) : {};
} catch (error) {
console.error("Error reading quota cache:", error);
return {};
}
}
function setQuotaCache(connectionId, quotaEntry) {
if (typeof window === "undefined") return;
try {
const cache = getQuotaCache();
cache[connectionId] = {
...quotaEntry,
cachedAt: new Date().toISOString(),
};
window.localStorage.setItem(QUOTA_CACHE_KEY, JSON.stringify(cache));
} catch (error) {
console.error("Error writing quota cache:", error);
}
}
const REFRESH_INTERVAL_MS = 60000; // 60 seconds
const DEPLETED_QUOTA_THRESHOLD = 5; // percent
const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh";
const ACCOUNT_FILTER_OPTIONS = [
{ value: "all", label: "All accounts" },
{ value: "active", label: "Active" },
{ value: "inactive", label: "Turned off" },
];
const QUOTA_SORT_OPTIONS = [
{ value: "default", label: "Default quota order" },
{ value: "remaining-asc", label: "% quota: low to high" },
{ value: "remaining-desc", label: "% quota: high to low" },
];
const CONNECTIONS_PAGE_SIZE = 20;
const ACCOUNT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
const ACCOUNT_PAGE_SIZE_MAX = 500;
export default function ProviderLimits() {
const [connections, setConnections] = useState([]);
const [quotaData, setQuotaData] = useState({});
@@ -1,5 +1,212 @@
import { getModelsByProviderId } from "open-sse/config/providerModels.js";
// ─── Constants ───────────────────────────────────────────────────────────────
export const QUOTA_CACHE_KEY = "quotaCacheData";
export const REFRESH_INTERVAL_MS = 60000;
export const DEPLETED_QUOTA_THRESHOLD = 5;
export const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh";
export const CONNECTIONS_PAGE_SIZE = 20;
export const ACCOUNT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
export const ACCOUNT_PAGE_SIZE_MAX = 500;
export const ACCOUNT_FILTER_OPTIONS = [
{ value: "all", label: "All accounts" },
{ value: "active", label: "Active" },
{ value: "inactive", label: "Turned off" },
];
export const QUOTA_SORT_OPTIONS = [
{ value: "default", label: "Default quota order" },
{ value: "remaining-asc", label: "% quota: low to high" },
{ value: "remaining-desc", label: "% quota: high to low" },
];
// ─── Pure helpers ─────────────────────────────────────────────────────────────
export function getConnectionLabel(connection) {
const isEmail = (value) =>
typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
if (isEmail(connection.email)) return connection.email;
if (isEmail(connection.name)) return connection.name;
return connection.name;
}
export function getConnectionQuotaRemaining(connection, quotaData) {
const quota = quotaData[connection.id]?.quotas?.[0];
if (!quota) return Number.POSITIVE_INFINITY;
if (typeof quota.remaining === "number") return quota.remaining;
return Number.POSITIVE_INFINITY;
}
export function sortVisibleConnections(
connections,
quotaData,
expiringFirst,
providerFilter,
quotaSortMode,
) {
if (providerFilter === "codex" && quotaSortMode !== "default") {
return [...connections].sort((a, b) => {
const remainingA = getConnectionQuotaRemaining(a, quotaData);
const remainingB = getConnectionQuotaRemaining(b, quotaData);
const remainingDiff =
quotaSortMode === "remaining-asc"
? remainingA - remainingB
: remainingB - remainingA;
if (remainingDiff !== 0) return remainingDiff;
return (getConnectionLabel(a) || "").localeCompare(
getConnectionLabel(b) || "",
);
});
}
if (!expiringFirst) return connections;
const getEarliestResetTime = (connection) => {
const resetTimes = (quotaData[connection.id]?.quotas || [])
.map((quota) =>
quota.resetAt
? new Date(quota.resetAt).getTime()
: Number.POSITIVE_INFINITY,
)
.filter((time) => Number.isFinite(time));
return resetTimes.length > 0
? Math.min(...resetTimes)
: Number.POSITIVE_INFINITY;
};
return [...connections].sort((a, b) => {
const expiryDiff = getEarliestResetTime(a) - getEarliestResetTime(b);
if (expiryDiff !== 0) return expiryDiff;
return (
(a.provider || "").localeCompare(b.provider || "") ||
(getConnectionLabel(a) || "").localeCompare(getConnectionLabel(b) || "")
);
});
}
export function buildLoadingState(connections) {
const nextLoadingState = {};
connections.forEach((connection) => {
nextLoadingState[connection.id] = true;
});
return nextLoadingState;
}
export function filterQuotaStateByConnections(state, connections) {
const visibleIds = new Set(connections.map((connection) => connection.id));
return Object.fromEntries(
Object.entries(state).filter(([id]) => visibleIds.has(id)),
);
}
export function getConnectionsPageRange(pagination) {
if (!pagination.total) {
return { start: 0, end: 0 };
}
const start = (pagination.page - 1) * pagination.pageSize + 1;
const end = Math.min(pagination.page * pagination.pageSize, pagination.total);
return { start, end };
}
export function getConnectionsEmptyMessage(totals, providerFilter, accountFilter) {
if (!totals.eligibleConnections) {
return {
icon: "cloud_off",
title: "No Providers Connected",
description:
"Connect to providers with OAuth to track your API quota limits and usage.",
};
}
if (!totals.providerFilteredConnections) {
return {
icon: "filter_alt_off",
title: "No Accounts Match Current Filters",
description:
providerFilter === "all"
? "Try changing the account status filter to see more quota trackers."
: `No ${accountFilter === "inactive" ? "turned off" : accountFilter === "active" ? "active" : "matching"} accounts found for ${providerFilter}.`,
};
}
return {
icon: "filter_alt_off",
title: "No Accounts On This Page",
description:
"Try moving to another page or refreshing the current filters.",
};
}
export function sortRequestFromExpiringFirst(expiringFirst) {
return expiringFirst ? "expiring" : "priority";
}
export function getPageSizeLabel(pageSize, isCustomPageSize) {
return isCustomPageSize ? `Custom: ${pageSize} / page` : `${pageSize} / page`;
}
export function getConnectionsPaginationSummary(pagination) {
const { start, end } = getConnectionsPageRange(pagination);
return `Showing ${start}-${end} of ${pagination.total}`;
}
export function getSafePagination(pagination, fallbackPageSize) {
return (
pagination || {
page: 1,
pageSize: fallbackPageSize,
total: 0,
totalPages: 1,
}
);
}
export function getSafeTotals(totals, fallbackTotal = 0) {
return (
totals || {
eligibleConnections: fallbackTotal,
providerFilteredConnections: fallbackTotal,
}
);
}
export function shouldResetPage(previousValue, nextValue) {
return previousValue !== nextValue;
}
export function getPaginationPageValue(dataPagination, fallbackPage) {
return dataPagination?.page || fallbackPage;
}
export function getProviderOptions(dataProviderOptions) {
return dataProviderOptions || [];
}
export async function reconcileConnectionsPage(fetchConnections, targetPage) {
return await fetchConnections(targetPage);
}
export function getQuotaCache() {
if (typeof window === "undefined") return {};
try {
const cached = window.localStorage.getItem(QUOTA_CACHE_KEY);
return cached ? JSON.parse(cached) : {};
} catch (error) {
console.error("Error reading quota cache:", error);
return {};
}
}
export function setQuotaCache(connectionId, quotaEntry) {
if (typeof window === "undefined") return;
try {
const cache = getQuotaCache();
cache[connectionId] = {
...quotaEntry,
cachedAt: new Date().toISOString(),
};
window.localStorage.setItem(QUOTA_CACHE_KEY, JSON.stringify(cache));
} catch (error) {
console.error("Error writing quota cache:", error);
}
}
/**
* Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit)
* @param {string|Date} date - ISO date string or Date object
+3 -2
View File
@@ -1,5 +1,6 @@
import { PROVIDER_MODELS } from "open-sse/config/providerModels.js";
import { AI_PROVIDERS, ALIAS_TO_ID } from "@/shared/constants/providers";
import { getModelKind } from "@/shared/constants/models";
const KIND_ENDPOINT = {
llm: "/v1/chat/completions",
@@ -52,10 +53,10 @@ function lookup(fullId, requestedKind) {
// PROVIDER_MODELS lookup (by alias key, fallback to providerId)
const list = PROVIDER_MODELS[alias] || PROVIDER_MODELS[providerId] || [];
const m = requestedKind
? list.find((x) => x.id === modelId && (x.kind || x.type || "llm") === requestedKind)
? list.find((x) => x.id === modelId && getModelKind(x, "llm") === requestedKind)
: list.find((x) => x.id === modelId);
if (m) {
const kind = m.kind || m.type || "llm";
const kind = getModelKind(m, "llm");
return buildInfo({ alias, providerId, model: m, kind, providerInfo });
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS, getModelKind } from "@/shared/constants/models";
import {
AI_PROVIDERS,
getProviderAlias,
@@ -316,7 +316,7 @@ export async function buildModelsList(kindFilter) {
const customModelIds = customModels
.filter((m) => {
if (!m?.id || ((m.kind || m.type) && (m.kind || m.type) !== "llm")) return false;
if (!m?.id || (getModelKind(m) && getModelKind(m) !== "llm")) return false;
const alias = m.providerAlias;
return alias === staticAlias || alias === outputAlias || alias === providerId;
})