diff --git a/src/browser/components/Settings/sections/ProvidersSection.tsx b/src/browser/components/Settings/sections/ProvidersSection.tsx
index fb7808cc4c..3c6a459f1b 100644
--- a/src/browser/components/Settings/sections/ProvidersSection.tsx
+++ b/src/browser/components/Settings/sections/ProvidersSection.tsx
@@ -21,6 +21,10 @@ import { useAPI } from "@/browser/contexts/API";
import { useSettings } from "@/browser/contexts/SettingsContext";
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig";
+import {
+ formatMuxGatewayBalance,
+ useMuxGatewayAccountStatus,
+} from "@/browser/hooks/useMuxGatewayAccountStatus";
import { useGateway } from "@/browser/hooks/useGatewayModels";
import { getEligibleGatewayModels } from "@/browser/utils/gatewayModels";
import { Button } from "@/browser/components/ui/button";
@@ -144,6 +148,12 @@ export function ProvidersSection() {
const { api } = useAPI();
const { config, updateOptimistically } = useProvidersConfig();
+ const {
+ data: muxGatewayAccountStatus,
+ error: muxGatewayAccountError,
+ isLoading: muxGatewayAccountLoading,
+ refresh: refreshMuxGatewayAccountStatus,
+ } = useMuxGatewayAccountStatus();
const gateway = useGateway();
@@ -318,6 +328,7 @@ export function ProvidersSection() {
}
setMuxGatewayLoginStatus("success");
+ void refreshMuxGatewayAccountStatus();
return;
}
@@ -426,6 +437,7 @@ export function ProvidersSection() {
}
setMuxGatewayLoginStatus("success");
+ void refreshMuxGatewayAccountStatus();
return;
}
@@ -444,6 +456,7 @@ export function ProvidersSection() {
api,
config,
applyGatewayModels,
+ refreshMuxGatewayAccountStatus,
]);
const muxGatewayCouponCodeSet = config?.["mux-gateway"]?.couponCodeSet ?? false;
const muxGatewayLoginInProgress =
@@ -471,6 +484,30 @@ export function ProvidersSection() {
setExpandedProvider(providersExpandedProvider);
setProvidersExpandedProvider(null);
}, [providersExpandedProvider, setProvidersExpandedProvider]);
+
+ useEffect(() => {
+ if (expandedProvider !== "mux-gateway" || !muxGatewayIsLoggedIn) {
+ return;
+ }
+
+ // Fetch lazily when the user expands the Mux Gateway provider.
+ //
+ // Important: avoid auto-retrying after a failure. If the request fails,
+ // `muxGatewayAccountStatus` remains null and we'd otherwise trigger a refresh
+ // on every render while the provider stays expanded.
+ if (muxGatewayAccountStatus || muxGatewayAccountLoading || muxGatewayAccountError) {
+ return;
+ }
+
+ void refreshMuxGatewayAccountStatus();
+ }, [
+ expandedProvider,
+ muxGatewayAccountError,
+ muxGatewayAccountLoading,
+ muxGatewayAccountStatus,
+ muxGatewayIsLoggedIn,
+ refreshMuxGatewayAccountStatus,
+ ]);
const [editingField, setEditingField] = useState<{
provider: string;
field: string;
@@ -706,6 +743,46 @@ export function ProvidersSection() {
)}
+ {provider === "mux-gateway" && muxGatewayIsLoggedIn && (
+
+
+
+
+
+ Balance and limits from Mux Gateway
+
+
+
+
+
+
+ Balance
+
+ {formatMuxGatewayBalance(muxGatewayAccountStatus?.remaining_microdollars)}
+
+
+
+
+ Concurrent requests per user
+
+ {muxGatewayAccountStatus?.ai_gateway_concurrent_requests_per_user ?? "—"}
+
+
+
+ {muxGatewayAccountError && (
+
{muxGatewayAccountError}
+ )}
+
+ )}
{fields.map((fieldConfig) => {
const isEditing =
editingField?.provider === provider && editingField?.field === fieldConfig.key;
diff --git a/src/browser/components/TitleBar.tsx b/src/browser/components/TitleBar.tsx
index 306ccf6f1c..2e20b0d3a7 100644
--- a/src/browser/components/TitleBar.tsx
+++ b/src/browser/components/TitleBar.tsx
@@ -2,6 +2,14 @@ import React, { useState, useEffect, useRef } from "react";
import { cn } from "@/common/lib/utils";
import { VERSION } from "@/version";
import { SettingsButton } from "./SettingsButton";
+import { GatewayIcon } from "./icons/GatewayIcon";
+import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig";
+import {
+ formatMuxGatewayBalance,
+ useMuxGatewayAccountStatus,
+} from "@/browser/hooks/useMuxGatewayAccountStatus";
+import { Button } from "./ui/button";
+import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
import type { UpdateStatus } from "@/common/orpc/types";
import { Download, Loader2, RefreshCw, ShieldCheck } from "lucide-react";
@@ -66,6 +74,17 @@ export function TitleBar() {
const { api } = useAPI();
const policyState = usePolicy();
const policyEnforced = policyState.status.state === "enforced";
+
+ const { config: providersConfig } = useProvidersConfig();
+ const muxGatewayIsLoggedIn = providersConfig?.["mux-gateway"]?.couponCodeSet ?? false;
+ const {
+ data: muxGatewayAccountStatus,
+ error: muxGatewayAccountError,
+ isLoading: muxGatewayAccountLoading,
+ refresh: refreshMuxGatewayAccountStatus,
+ } = useMuxGatewayAccountStatus();
+ const [muxGatewayPopoverOpen, setMuxGatewayPopoverOpen] = useState(false);
+
const { extendedTimestamp, gitDescribe } = parseBuildInfo(VERSION satisfies unknown);
const [updateStatus, setUpdateStatus] = useState({ type: "idle" });
const [isCheckingOnHover, setIsCheckingOnHover] = useState(false);
@@ -275,6 +294,65 @@ export function TitleBar() {
+ {muxGatewayIsLoggedIn && (
+
{
+ setMuxGatewayPopoverOpen(open);
+ if (open) {
+ void refreshMuxGatewayAccountStatus();
+ }
+ }}
+ >
+
+
+
+
+ Mux Gateway
+
+
+
+ Balance
+
+ {formatMuxGatewayBalance(muxGatewayAccountStatus?.remaining_microdollars)}
+
+
+
+
+ Concurrent requests per user
+
+ {muxGatewayAccountStatus?.ai_gateway_concurrent_requests_per_user ?? "—"}
+
+
+
+
+ {muxGatewayAccountError && (
+ {muxGatewayAccountError}
+ )}
+
+
+
+
+
+
+ )}
+
{policyEnforced && (
diff --git a/src/browser/components/splashScreens/OnboardingWizardSplash.tsx b/src/browser/components/splashScreens/OnboardingWizardSplash.tsx
index aacee94ff6..4d520bce54 100644
--- a/src/browser/components/splashScreens/OnboardingWizardSplash.tsx
+++ b/src/browser/components/splashScreens/OnboardingWizardSplash.tsx
@@ -31,6 +31,10 @@ import { updatePersistedState } from "@/browser/hooks/usePersistedState";
import { getEligibleGatewayModels } from "@/browser/utils/gatewayModels";
import type { ProvidersConfigMap } from "@/common/orpc/types";
import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig";
+import {
+ formatMuxGatewayBalance,
+ useMuxGatewayAccountStatus,
+} from "@/browser/hooks/useMuxGatewayAccountStatus";
import { KEYBINDS, formatKeybind } from "@/browser/utils/ui/keybinds";
import { getAgentsInitNudgeKey } from "@/common/constants/storage";
import { PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers";
@@ -201,6 +205,12 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
const [direction, setDirection] = useState("forward");
const { api } = useAPI();
+ const {
+ data: muxGatewayAccountStatus,
+ error: muxGatewayAccountError,
+ isLoading: muxGatewayAccountLoading,
+ refresh: refreshMuxGatewayAccountStatus,
+ } = useMuxGatewayAccountStatus();
const backendBaseUrl = getBackendBaseUrl();
const backendOrigin = useMemo(() => {
@@ -305,6 +315,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
}
setMuxGatewayLoginStatus("success");
+ void refreshMuxGatewayAccountStatus();
return;
}
@@ -375,7 +386,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
setMuxGatewayLoginStatus("error");
setMuxGatewayLoginError(message);
}
- }, [api, backendBaseUrl, isDesktop, providersConfig]);
+ }, [api, backendBaseUrl, isDesktop, providersConfig, refreshMuxGatewayAccountStatus]);
useEffect(() => {
const attempt = muxGatewayLoginAttemptRef.current;
@@ -413,6 +424,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
}
setMuxGatewayLoginStatus("success");
+ void refreshMuxGatewayAccountStatus();
return;
}
@@ -430,6 +442,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
muxGatewayLoginStatus,
muxGatewayServerState,
providersConfig,
+ refreshMuxGatewayAccountStatus,
]);
const muxGatewayCouponCodeSet = providersConfig?.["mux-gateway"]?.couponCodeSet ?? false;
@@ -522,33 +535,74 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
use this to get free evaluation credits.
-
-
-
- {muxGatewayLoginInProgress && (
-
- )}
-
-
- {muxGatewayLoginStatus === "success" && Login successful.
}
-
- {muxGatewayLoginStatus === "waiting" && (
- Finish the login flow in your browser, then return here.
- )}
-
- {muxGatewayLoginStatus === "error" && muxGatewayLoginError && (
-
- Login failed: {muxGatewayLoginError}
-
+ {muxGatewayIsLoggedIn ? (
+
+
+
+
Mux Gateway account
+
+
+
+
+
+ Balance
+
+ {formatMuxGatewayBalance(muxGatewayAccountStatus?.remaining_microdollars)}
+
+
+
+
+ Concurrent requests per user
+
+ {muxGatewayAccountStatus?.ai_gateway_concurrent_requests_per_user ?? "—"}
+
+
+
+
+ {muxGatewayAccountError && (
+
{muxGatewayAccountError}
+ )}
+
+
+ ) : (
+ <>
+
+
+
+ {muxGatewayLoginInProgress && (
+
+ )}
+
+
+ {muxGatewayLoginStatus === "waiting" && (
+ Finish the login flow in your browser, then return here.
+ )}
+
+ {muxGatewayLoginStatus === "error" && muxGatewayLoginError && (
+
+ Login failed:{" "}
+ {muxGatewayLoginError}
+
+ )}
+ >
)}
You can also receive those credits through:
@@ -845,6 +899,10 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
configuredProvidersSummary,
cycleAgentShortcut,
hasConfiguredProvidersAtStart,
+ muxGatewayAccountError,
+ muxGatewayAccountLoading,
+ muxGatewayAccountStatus,
+ muxGatewayIsLoggedIn,
muxGatewayLoginButtonLabel,
muxGatewayLoginError,
muxGatewayLoginInProgress,
@@ -852,6 +910,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
openSettings,
projects.size,
providersConfig,
+ refreshMuxGatewayAccountStatus,
startMuxGatewayLogin,
visibleProviders,
]);
diff --git a/src/browser/hooks/useMuxGatewayAccountStatus.ts b/src/browser/hooks/useMuxGatewayAccountStatus.ts
new file mode 100644
index 0000000000..df9c5f2f6d
--- /dev/null
+++ b/src/browser/hooks/useMuxGatewayAccountStatus.ts
@@ -0,0 +1,49 @@
+import { useCallback, useState } from "react";
+import { useAPI } from "@/browser/contexts/API";
+import { formatCostWithDollar } from "@/common/utils/tokens/usageAggregator";
+
+export interface MuxGatewayAccountStatus {
+ remaining_microdollars: number;
+ ai_gateway_concurrent_requests_per_user: number;
+}
+
+export function formatMuxGatewayBalance(remainingMicrodollars: number | null | undefined): string {
+ if (remainingMicrodollars === null || remainingMicrodollars === undefined) {
+ return "—";
+ }
+
+ return formatCostWithDollar(remainingMicrodollars / 1_000_000);
+}
+
+export function useMuxGatewayAccountStatus() {
+ const { api } = useAPI();
+ const [data, setData] = useState(null);
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const refresh = useCallback(async () => {
+ if (!api) {
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const result = await api.muxGateway.getAccountStatus();
+ if (result.success) {
+ setData(result.data);
+ return;
+ }
+
+ setError(result.error);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ setError(message);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [api]);
+
+ return { data, error, isLoading, refresh };
+}
diff --git a/src/browser/stories/mocks/orpc.ts b/src/browser/stories/mocks/orpc.ts
index ec372eba6f..00dafab452 100644
--- a/src/browser/stories/mocks/orpc.ts
+++ b/src/browser/stories/mocks/orpc.ts
@@ -513,6 +513,16 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
setProviderConfig: () => Promise.resolve({ success: true, data: undefined }),
setModels: () => Promise.resolve({ success: true, data: undefined }),
},
+ muxGateway: {
+ getAccountStatus: () =>
+ Promise.resolve({
+ success: true,
+ data: {
+ remaining_microdollars: 134_598_127,
+ ai_gateway_concurrent_requests_per_user: 20,
+ },
+ }),
+ },
general: {
listDirectory: () => Promise.resolve({ entries: [], hasMore: false }),
ping: (input: string) => Promise.resolve(`Pong: ${input}`),
diff --git a/src/common/orpc/schemas.ts b/src/common/orpc/schemas.ts
index a94da45a89..6406cb6682 100644
--- a/src/common/orpc/schemas.ts
+++ b/src/common/orpc/schemas.ts
@@ -178,6 +178,7 @@ export {
nameGeneration,
projects,
ProviderConfigInfoSchema,
+ muxGateway,
muxGatewayOauth,
policy,
providers,
diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts
index cb59593564..5749e6bb79 100644
--- a/src/common/orpc/schemas/api.ts
+++ b/src/common/orpc/schemas/api.ts
@@ -196,6 +196,20 @@ export const muxGatewayOauth = {
},
};
+// Mux Gateway
+export const muxGateway = {
+ getAccountStatus: {
+ input: z.void(),
+ output: ResultSchema(
+ z.object({
+ remaining_microdollars: z.number().int().nonnegative(),
+ ai_gateway_concurrent_requests_per_user: z.number().int().nonnegative(),
+ }),
+ z.string()
+ ),
+ },
+};
+
// Projects
export const projects = {
create: {
diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts
index aa08d83ea9..9d5ea40c8a 100644
--- a/src/node/orpc/router.ts
+++ b/src/node/orpc/router.ts
@@ -1,6 +1,9 @@
import { os } from "@orpc/server";
import * as schemas from "@/common/orpc/schemas";
import type { ORPCContext } from "./context";
+import { MUX_GATEWAY_ORIGIN } from "@/common/constants/muxGatewayOAuth";
+import { Err, Ok } from "@/common/types/result";
+import { resolveProviderCredentials } from "@/node/utils/providerRequirements";
import {
selectModelForNameGeneration,
generateWorkspaceIdentity,
@@ -664,6 +667,86 @@ export const router = (authToken?: string) => {
}
}),
},
+ muxGateway: {
+ getAccountStatus: t
+ .input(schemas.muxGateway.getAccountStatus.input)
+ .output(schemas.muxGateway.getAccountStatus.output)
+ .handler(async ({ context }) => {
+ const providersConfig = context.config.loadProvidersConfig() ?? {};
+ const muxConfig = (providersConfig["mux-gateway"] ?? {}) as Record;
+ const creds = resolveProviderCredentials("mux-gateway", {
+ couponCode: typeof muxConfig.couponCode === "string" ? muxConfig.couponCode : undefined,
+ voucher: typeof muxConfig.voucher === "string" ? muxConfig.voucher : undefined,
+ });
+
+ if (!creds.isConfigured || !creds.couponCode) {
+ return Err("Mux Gateway is not logged in");
+ }
+
+ let response: Awaited>;
+ try {
+ response = await fetch(`${MUX_GATEWAY_ORIGIN}/api/v1/balance`, {
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${creds.couponCode}`,
+ },
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return Err(`Mux Gateway balance request failed: ${message}`);
+ }
+
+ if (!response.ok) {
+ let body = "";
+ try {
+ body = await response.text();
+ } catch {
+ // Ignore errors reading response body
+ }
+ const prefix = body.trim().slice(0, 200);
+ return Err(
+ `Mux Gateway balance request failed (HTTP ${response.status}): ${
+ prefix || response.statusText
+ }`
+ );
+ }
+
+ let json: unknown;
+ try {
+ json = await response.json();
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return Err(`Mux Gateway balance response was not valid JSON: ${message}`);
+ }
+
+ const payload = json as {
+ remaining_microdollars?: unknown;
+ ai_gateway_concurrent_requests_per_user?: unknown;
+ };
+
+ const remaining = payload.remaining_microdollars;
+ const concurrency = payload.ai_gateway_concurrent_requests_per_user;
+
+ if (
+ typeof remaining !== "number" ||
+ !Number.isFinite(remaining) ||
+ !Number.isInteger(remaining) ||
+ remaining < 0 ||
+ typeof concurrency !== "number" ||
+ !Number.isFinite(concurrency) ||
+ !Number.isInteger(concurrency) ||
+ concurrency < 0
+ ) {
+ return Err("Mux Gateway returned an invalid balance payload");
+ }
+
+ return Ok({
+ remaining_microdollars: remaining,
+ ai_gateway_concurrent_requests_per_user: concurrency,
+ });
+ }),
+ },
+
muxGatewayOauth: {
startDesktopFlow: t
.input(schemas.muxGatewayOauth.startDesktopFlow.input)