From 8391055d71f234f561fd010c5caf1ff0145e1e20 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 29 Jan 2026 07:33:41 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20feat:=20show=20Mux=20Gateway?= =?UTF-8?q?=20balance=20+=20limits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose /api/v1/balance via oRPC and display balance + concurrency in onboarding, title bar, and provider settings. --- _Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh` • Cost: `$3.70`_ --- .../Settings/sections/ProvidersSection.tsx | 72 +++++++++++ src/browser/components/TitleBar.tsx | 78 ++++++++++++ .../splashScreens/OnboardingWizardSplash.tsx | 115 +++++++++++++----- .../hooks/useMuxGatewayAccountStatus.ts | 49 ++++++++ src/browser/stories/mocks/orpc.ts | 10 ++ src/common/orpc/schemas.ts | 1 + src/common/orpc/schemas/api.ts | 14 +++ src/node/orpc/router.ts | 83 +++++++++++++ 8 files changed, 394 insertions(+), 28 deletions(-) create mode 100644 src/browser/hooks/useMuxGatewayAccountStatus.ts diff --git a/src/browser/components/Settings/sections/ProvidersSection.tsx b/src/browser/components/Settings/sections/ProvidersSection.tsx index fb7808cc4c..74591683a3 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,25 @@ 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. + if (muxGatewayAccountStatus || muxGatewayAccountLoading) { + return; + } + + void refreshMuxGatewayAccountStatus(); + }, [ + expandedProvider, + muxGatewayAccountLoading, + muxGatewayAccountStatus, + muxGatewayIsLoggedIn, + refreshMuxGatewayAccountStatus, + ]); const [editingField, setEditingField] = useState<{ provider: string; field: string; @@ -706,6 +738,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) From c3464bac4827034d1d6cb6d82ac7aa1225710188 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:07:16 +0000 Subject: [PATCH 2/2] fix: avoid auto-retrying Mux Gateway balance fetch --- .../components/Settings/sections/ProvidersSection.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/browser/components/Settings/sections/ProvidersSection.tsx b/src/browser/components/Settings/sections/ProvidersSection.tsx index 74591683a3..3c6a459f1b 100644 --- a/src/browser/components/Settings/sections/ProvidersSection.tsx +++ b/src/browser/components/Settings/sections/ProvidersSection.tsx @@ -491,13 +491,18 @@ export function ProvidersSection() { } // Fetch lazily when the user expands the Mux Gateway provider. - if (muxGatewayAccountStatus || muxGatewayAccountLoading) { + // + // 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,