feat: add STT support, Gemini TTS, and expand usage tracking

- Speech-to-Text: full pipeline with sttCore handler, /v1/audio/transcriptions
  endpoint, sttConfig for OpenAI, Gemini, Groq, Deepgram, AssemblyAI,
  HuggingFace, NVIDIA Parakeet; new 9router-stt skill
- Gemini TTS: add gemini provider with 30 prebuilt voices and TTS_PROVIDER_CONFIG
- Usage: implement GLM (intl/cn) and MiniMax (intl/cn) quota fetchers; refactor
  Gemini CLI usage to use retrieveUserQuota with per-model buckets
- Disabled models: lowdb-backed disabledModelsDb + /api/models/disabled route
- Header search: reusable Zustand store (headerSearchStore) wired into Header
- CLI tools: add Claude Cowork tool card and cowork-settings API
- Providers: introduce mediaPriority sorting in getProvidersByKind, add
  Kimi K2.6, reorder hermes, drop qwen STT kind
- UI: expand media-providers/[kind]/[id] page (+314), enhance OAuthModal,
  ModelSelectModal, ProviderTopology, ProxyPools, ProviderLimits
- Assets: refresh provider PNGs (alicode, byteplus, cloudflare-ai, nvidia,
  ollama, vertex, volcengine-ark) and add aws-polly, fal-ai, jina-ai, recraft,
  runwayml, stability-ai, topaz, black-forest-labs
This commit is contained in:
decolua committed 2026-05-05 10:32:59 +07:00
1 parent bfb7d42164
commit d4bc42e1f5
67 files changed
+2930 -234

No files matched your search

+67 -17
View File
@@ -24,6 +24,7 @@ import {
import Link from "next/link";
import { getErrorCode, getRelativeTime } from "@/shared/utils";
import { useNotificationStore } from "@/store/notificationStore";
import { useHeaderSearchStore } from "@/store/headerSearchStore";
import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge";
function getStatusDisplay(connected, error, errorCode) {
@@ -103,6 +104,18 @@ export default function ProvidersPage() {
const [testingMode, setTestingMode] = useState(null);
const [testResults, setTestResults] = useState(null);
const notify = useNotificationStore();
const searchQuery = useHeaderSearchStore((s) => s.query);
const registerSearch = useHeaderSearchStore((s) => s.register);
const unregisterSearch = useHeaderSearchStore((s) => s.unregister);
useEffect(() => {
registerSearch("Search providers...");
return () => unregisterSearch();
}, [registerSearch, unregisterSearch]);
const matchSearch = (name) =>
!searchQuery.trim() ||
name.toLowerCase().includes(searchQuery.trim().toLowerCase());
useEffect(() => {
const fetchData = async () => {
@@ -224,7 +237,8 @@ export default function ProvidersPage() {
color: "#10A37F",
textIcon: "OC",
apiType: node.apiType,
}));
}))
.filter((p) => matchSearch(p.name));
const anthropicCompatibleProviders = providerNodes
.filter((node) => node.type === "anthropic-compatible")
@@ -233,7 +247,22 @@ export default function ProvidersPage() {
name: node.name || "Anthropic Compatible",
color: "#D97757",
textIcon: "AC",
}));
}))
.filter((p) => matchSearch(p.name));
const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter(([, info]) =>
matchSearch(info.name),
);
const freeEntries = Object.entries(FREE_PROVIDERS).filter(([, info]) =>
matchSearch(info.name),
);
const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS).filter(
([, info]) => matchSearch(info.name),
);
const apikeyEntries = Object.entries(APIKEY_PROVIDERS).filter(
([, info]) =>
(info.serviceKinds ?? ["llm"]).includes("llm") && matchSearch(info.name),
);
if (loading) {
return (
@@ -244,9 +273,27 @@ export default function ProvidersPage() {
);
}
const hasAnyResult =
oauthEntries.length > 0 ||
freeEntries.length > 0 ||
freeTierEntries.length > 0 ||
apikeyEntries.length > 0 ||
compatibleProviders.length > 0 ||
anthropicCompatibleProviders.length > 0;
return (
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
{!hasAnyResult && (
<div className="text-center py-8 border border-dashed border-border rounded-xl">
<span className="material-symbols-outlined text-[32px] text-text-muted mb-2">
search_off
</span>
<p className="text-text-muted text-sm">No providers match your search</p>
</div>
)}
{/* OAuth Providers */}
{oauthEntries.length > 0 && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
@@ -275,7 +322,7 @@ export default function ProvidersPage() {
</div>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
{Object.entries(OAUTH_PROVIDERS).map(([key, info]) => (
{oauthEntries.map(([key, info]) => (
<ProviderCard
key={key}
providerId={key}
@@ -287,8 +334,10 @@ export default function ProvidersPage() {
))}
</div>
</div>
)}
{/* Free Tier Providers */}
{(freeEntries.length > 0 || freeTierEntries.length > 0) && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
@@ -314,7 +363,7 @@ export default function ProvidersPage() {
</button>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
{Object.entries(FREE_PROVIDERS).map(([key, info]) => (
{freeEntries.map(([key, info]) => (
<ProviderCard
key={key}
providerId={key}
@@ -324,7 +373,7 @@ export default function ProvidersPage() {
onToggle={(active) => handleToggleProvider(key, "oauth", active)}
/>
))}
{Object.entries(FREE_TIER_PROVIDERS).map(([key, info]) => (
{freeTierEntries.map(([key, info]) => (
<ApiKeyProviderCard
key={key}
providerId={key}
@@ -336,8 +385,10 @@ export default function ProvidersPage() {
))}
</div>
</div>
)}
{/* API Key Providers — fixed list */}
{apikeyEntries.length > 0 && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
@@ -363,20 +414,19 @@ export default function ProvidersPage() {
</button>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
{Object.entries(APIKEY_PROVIDERS)
.filter(([, info]) => (info.serviceKinds ?? ["llm"]).includes("llm"))
.map(([key, info]) => (
<ApiKeyProviderCard
key={key}
providerId={key}
provider={info}
stats={getProviderStats(key, "apikey")}
authType="apikey"
onToggle={(active) => handleToggleProvider(key, "apikey", active)}
/>
))}
{apikeyEntries.map(([key, info]) => (
<ApiKeyProviderCard
key={key}
providerId={key}
provider={info}
stats={getProviderStats(key, "apikey")}
authType="apikey"
onToggle={(active) => handleToggleProvider(key, "apikey", active)}
/>
))}
</div>
</div>
)}
{/* Web Cookie Providers — use browser subscription cookie instead of API key */}
{/* <div className="flex flex-col gap-4">