From 0a83b514284feb1e55fabcc8b628b43ba1c2bf1e Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 6 Dec 2025 15:38:45 -0600 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20cloud=20icon?= =?UTF-8?q?=20toggle=20for=20Mux=20Gateway=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add whitespace-nowrap to ProviderWithIcon to prevent line breaks - Create useGatewayModels hook for managing gateway preferences - Add cloud icon toggle to ModelRow in settings - Add cloud icon toggle to ModelSelector dropdown - Transform model to gateway format when sending messages Users can now click the cloud icon on any model to route it through Mux Gateway. The preference is stored per-model and the transformation happens transparently when sending messages. _Generated with mux_ --- src/browser/components/ModelSelector.tsx | 72 ++++++++++++++---- src/browser/components/ProviderIcon.tsx | 2 +- .../components/Settings/sections/ModelRow.tsx | 25 ++++++- .../Settings/sections/ModelsSection.tsx | 6 ++ src/browser/hooks/useGatewayModels.ts | 74 +++++++++++++++++++ src/browser/hooks/useSendMessageOptions.ts | 6 +- src/browser/utils/messages/sendOptions.ts | 5 +- 7 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 src/browser/hooks/useGatewayModels.ts diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index 1e7509e6c..69517c0bb 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -7,9 +7,10 @@ import React, { forwardRef, } from "react"; import { cn } from "@/common/lib/utils"; -import { Settings, Star } from "lucide-react"; +import { Cloud, Settings, Star } from "lucide-react"; import { TooltipWrapper, Tooltip } from "./Tooltip"; import { useSettings } from "@/browser/contexts/SettingsContext"; +import { useGatewayModels } from "@/browser/hooks/useGatewayModels"; interface ModelSelectorProps { value: string; @@ -27,6 +28,7 @@ export interface ModelSelectorRef { export const ModelSelector = forwardRef( ({ value, onChange, recentModels, onComplete, defaultModel, onSetDefaultModel }, ref) => { const { open: openSettings } = useSettings(); + const { isEnabled: isGatewayEnabled, toggle: toggleGateway } = useGatewayModels(); const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(value); const [error, setError] = useState(null); @@ -190,8 +192,17 @@ export const ModelSelector = forwardRef( }, [highlightedIndex]); if (!isEditing) { + const gatewayEnabled = isGatewayEnabled(value); return (
+ {gatewayEnabled && ( + + + + Using Mux Gateway + + + )}
( )} onClick={() => handleSelectModel(model)} > -
+
{model} - {onSetDefaultModel && ( +
+ {/* Gateway toggle */} - {defaultModel === model - ? "Current default model" - : "Set as default model"} + {isGatewayEnabled(model) ? "Using Mux Gateway" : "Use Mux Gateway"} - )} + {/* Default model toggle */} + {onSetDefaultModel && ( + + + + {defaultModel === model + ? "Current default model" + : "Set as default model"} + + + )} +
)) diff --git a/src/browser/components/ProviderIcon.tsx b/src/browser/components/ProviderIcon.tsx index 0c94373ad..a120d17c8 100644 --- a/src/browser/components/ProviderIcon.tsx +++ b/src/browser/components/ProviderIcon.tsx @@ -67,7 +67,7 @@ export function ProviderWithIcon(props: ProviderWithIconProps) { : props.provider; return ( - + {name} diff --git a/src/browser/components/Settings/sections/ModelRow.tsx b/src/browser/components/Settings/sections/ModelRow.tsx index 418b8a6de..77505a0c5 100644 --- a/src/browser/components/Settings/sections/ModelRow.tsx +++ b/src/browser/components/Settings/sections/ModelRow.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Check, Pencil, Star, Trash2, X } from "lucide-react"; +import { Check, Cloud, Pencil, Star, Trash2, X } from "lucide-react"; import { cn } from "@/common/lib/utils"; import { TooltipWrapper, Tooltip } from "@/browser/components/Tooltip"; import { ProviderWithIcon } from "@/browser/components/ProviderIcon"; @@ -16,12 +16,16 @@ export interface ModelRowProps { editError?: string | null; saving?: boolean; hasActiveEdit?: boolean; + /** Whether gateway mode is enabled for this model */ + isGatewayEnabled?: boolean; onSetDefault: () => void; onStartEdit?: () => void; onSaveEdit?: () => void; onCancelEdit?: () => void; onEditChange?: (value: string) => void; onRemove?: () => void; + /** Toggle gateway mode for this model */ + onToggleGateway?: () => void; } export function ModelRow(props: ModelRowProps) { @@ -90,6 +94,25 @@ export function ModelRow(props: ModelRowProps) { ) : ( <> + {/* Gateway toggle button */} + {props.onToggleGateway && ( + + + + {props.isGatewayEnabled ? "Using Mux Gateway" : "Use Mux Gateway"} + + + )} {/* Favorite/default button */}
diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts new file mode 100644 index 000000000..4b41c44f0 --- /dev/null +++ b/src/browser/hooks/useGatewayModels.ts @@ -0,0 +1,74 @@ +import { useCallback } from "react"; +import { usePersistedState, readPersistedState, updatePersistedState } from "./usePersistedState"; + +const GATEWAY_MODELS_KEY = "gateway-models"; + +/** + * Check if a model is gateway-enabled (static read, no reactivity) + */ +export function isGatewayEnabled(modelId: string): boolean { + const gatewayModels = readPersistedState(GATEWAY_MODELS_KEY, []); + return gatewayModels.includes(modelId); +} + +/** + * Transform a model ID to gateway format if gateway is enabled for it. + * Example: "anthropic:claude-opus-4-5" → "mux-gateway:anthropic/claude-opus-4-5" + */ +export function toGatewayModel(modelId: string): string { + if (!isGatewayEnabled(modelId)) { + return modelId; + } + // Transform provider:model to mux-gateway:provider/model + const colonIndex = modelId.indexOf(":"); + if (colonIndex === -1) { + return modelId; + } + const provider = modelId.slice(0, colonIndex); + const model = modelId.slice(colonIndex + 1); + return `mux-gateway:${provider}/${model}`; +} + +/** + * Toggle gateway mode for a model (static update, no reactivity) + */ +export function toggleGatewayModel(modelId: string): void { + const gatewayModels = readPersistedState(GATEWAY_MODELS_KEY, []); + if (gatewayModels.includes(modelId)) { + updatePersistedState( + GATEWAY_MODELS_KEY, + gatewayModels.filter((m) => m !== modelId) + ); + } else { + updatePersistedState(GATEWAY_MODELS_KEY, [...gatewayModels, modelId]); + } +} + +/** + * Hook to manage which models use the Mux Gateway. + * Returns reactive state and toggle function. + */ +export function useGatewayModels() { + const [gatewayModels, setGatewayModels] = usePersistedState(GATEWAY_MODELS_KEY, [], { + listener: true, + }); + + const isEnabled = useCallback( + (modelId: string) => gatewayModels.includes(modelId), + [gatewayModels] + ); + + const toggle = useCallback( + (modelId: string) => { + setGatewayModels((prev) => { + if (prev.includes(modelId)) { + return prev.filter((m) => m !== modelId); + } + return [...prev, modelId]; + }); + }, + [setGatewayModels] + ); + + return { gatewayModels, isEnabled, toggle }; +} diff --git a/src/browser/hooks/useSendMessageOptions.ts b/src/browser/hooks/useSendMessageOptions.ts index f848a8eb4..39bcb3e13 100644 --- a/src/browser/hooks/useSendMessageOptions.ts +++ b/src/browser/hooks/useSendMessageOptions.ts @@ -2,6 +2,7 @@ import { useThinkingLevel } from "./useThinkingLevel"; import { useMode } from "@/browser/contexts/ModeContext"; import { usePersistedState } from "./usePersistedState"; import { getDefaultModel } from "./useModelLRU"; +import { toGatewayModel } from "./useGatewayModels"; import { modeToToolPolicy, PLAN_MODE_INSTRUCTION } from "@/common/utils/ui/modeUtils"; import { getModelKey } from "@/common/constants/storage"; import type { SendMessageOptions } from "@/common/orpc/types"; @@ -26,9 +27,12 @@ function constructSendMessageOptions( const additionalSystemInstructions = mode === "plan" ? PLAN_MODE_INSTRUCTION : undefined; // Ensure model is always a valid string (defensive against corrupted localStorage) - const model = + const baseModel = typeof preferredModel === "string" && preferredModel ? preferredModel : fallbackModel; + // Transform to gateway format if gateway is enabled for this model + const model = toGatewayModel(baseModel); + // Enforce thinking policy at the UI boundary as well (e.g., gpt-5-pro → high only) const uiThinking = enforceThinkingPolicy(model, thinkingLevel); diff --git a/src/browser/utils/messages/sendOptions.ts b/src/browser/utils/messages/sendOptions.ts index e8bfb07c7..5573297ad 100644 --- a/src/browser/utils/messages/sendOptions.ts +++ b/src/browser/utils/messages/sendOptions.ts @@ -2,6 +2,7 @@ import { getModelKey, getThinkingLevelKey, getModeKey } from "@/common/constants import { modeToToolPolicy, PLAN_MODE_INSTRUCTION } from "@/common/utils/ui/modeUtils"; import { readPersistedState } from "@/browser/hooks/usePersistedState"; import { getDefaultModel } from "@/browser/hooks/useModelLRU"; +import { toGatewayModel } from "@/browser/hooks/useGatewayModels"; import type { SendMessageOptions } from "@/common/orpc/types"; import type { UIMode } from "@/common/types/mode"; import type { ThinkingLevel } from "@/common/types/thinking"; @@ -38,7 +39,9 @@ function getProviderOptions(): MuxProviderOptions { */ export function getSendOptionsFromStorage(workspaceId: string): SendMessageOptions { // Read model preference (workspace-specific), fallback to LRU default - const model = readPersistedState(getModelKey(workspaceId), getDefaultModel()); + const baseModel = readPersistedState(getModelKey(workspaceId), getDefaultModel()); + // Transform to gateway format if gateway is enabled for this model + const model = toGatewayModel(baseModel); // Read thinking level (workspace-specific) const thinkingLevel = readPersistedState( From 04208be7bfef1e8afd8aa22b34fee7152eb4c16b Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 6 Dec 2025 15:43:03 -0600 Subject: [PATCH 02/16] fix: fallback to direct provider when gateway not configured - Track gateway availability (couponCodeSet) in localStorage - Only transform to gateway format when gateway is both enabled AND available - Hide cloud icon toggle when gateway provider is not configured - Cloud icon only shows in model selector when gateway is actually active --- src/browser/components/ModelSelector.tsx | 66 ++++++++++--------- .../Settings/sections/ModelsSection.tsx | 10 ++- src/browser/hooks/useGatewayModels.ts | 35 ++++++++-- 3 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index 69517c0bb..d76565a95 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -28,7 +28,11 @@ export interface ModelSelectorRef { export const ModelSelector = forwardRef( ({ value, onChange, recentModels, onComplete, defaultModel, onSetDefaultModel }, ref) => { const { open: openSettings } = useSettings(); - const { isEnabled: isGatewayEnabled, toggle: toggleGateway } = useGatewayModels(); + const { + isEnabled: isGatewayEnabled, + toggle: toggleGateway, + gatewayAvailable, + } = useGatewayModels(); const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(value); const [error, setError] = useState(null); @@ -192,7 +196,7 @@ export const ModelSelector = forwardRef( }, [highlightedIndex]); if (!isEditing) { - const gatewayEnabled = isGatewayEnabled(value); + const gatewayEnabled = isGatewayEnabled(value) && gatewayAvailable; return (
{gatewayEnabled && ( @@ -264,34 +268,36 @@ export const ModelSelector = forwardRef(
{model}
- {/* Gateway toggle */} - - - - {isGatewayEnabled(model) ? "Using Mux Gateway" : "Use Mux Gateway"} - - + {/* Gateway toggle - only show when gateway is configured */} + {gatewayAvailable && ( + + + + {isGatewayEnabled(model) ? "Using Mux Gateway" : "Use Mux Gateway"} + + + )} {/* Default model toggle */} {onSetDefaultModel && ( diff --git a/src/browser/components/Settings/sections/ModelsSection.tsx b/src/browser/components/Settings/sections/ModelsSection.tsx index 0f9ebdb5f..0b9af6c3e 100644 --- a/src/browser/components/Settings/sections/ModelsSection.tsx +++ b/src/browser/components/Settings/sections/ModelsSection.tsx @@ -26,7 +26,11 @@ export function ModelsSection() { const [editing, setEditing] = useState(null); const [error, setError] = useState(null); const { defaultModel, setDefaultModel } = useModelLRU(); - const { isEnabled: isGatewayEnabled, toggle: toggleGateway } = useGatewayModels(); + const { + isEnabled: isGatewayEnabled, + toggle: toggleGateway, + gatewayAvailable, + } = useGatewayModels(); // Check if a model already exists (for duplicate prevention) const modelExists = useCallback( @@ -224,7 +228,7 @@ export function ModelsSection() { setEditing((prev) => (prev ? { ...prev, newModelId: value } : null)) } onRemove={() => handleRemoveModel(model.provider, model.modelId)} - onToggleGateway={() => toggleGateway(model.fullId)} + onToggleGateway={gatewayAvailable ? () => toggleGateway(model.fullId) : undefined} /> ); })} @@ -247,7 +251,7 @@ export function ModelsSection() { isEditing={false} isGatewayEnabled={isGatewayEnabled(model.fullId)} onSetDefault={() => setDefaultModel(model.fullId)} - onToggleGateway={() => toggleGateway(model.fullId)} + onToggleGateway={gatewayAvailable ? () => toggleGateway(model.fullId) : undefined} /> ))}
diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index 4b41c44f0..84fd86706 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -1,7 +1,9 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { usePersistedState, readPersistedState, updatePersistedState } from "./usePersistedState"; +import { useProvidersConfig } from "./useProvidersConfig"; const GATEWAY_MODELS_KEY = "gateway-models"; +const GATEWAY_AVAILABLE_KEY = "gateway-available"; /** * Check if a model is gateway-enabled (static read, no reactivity) @@ -12,11 +14,20 @@ export function isGatewayEnabled(modelId: string): boolean { } /** - * Transform a model ID to gateway format if gateway is enabled for it. + * Check if the gateway provider is available (has coupon code configured) + */ +export function isGatewayAvailable(): boolean { + return readPersistedState(GATEWAY_AVAILABLE_KEY, false); +} + +/** + * Transform a model ID to gateway format if gateway is enabled AND available. + * Falls back to direct provider if gateway is not configured. * Example: "anthropic:claude-opus-4-5" → "mux-gateway:anthropic/claude-opus-4-5" */ export function toGatewayModel(modelId: string): string { - if (!isGatewayEnabled(modelId)) { + // Only transform if user enabled gateway for this model AND gateway is configured + if (!isGatewayEnabled(modelId) || !isGatewayAvailable()) { return modelId; } // Transform provider:model to mux-gateway:provider/model @@ -47,11 +58,27 @@ export function toggleGatewayModel(modelId: string): void { /** * Hook to manage which models use the Mux Gateway. * Returns reactive state and toggle function. + * + * Also syncs gateway availability from provider config to localStorage + * so that toGatewayModel() can check it synchronously. */ export function useGatewayModels() { + const { config } = useProvidersConfig(); const [gatewayModels, setGatewayModels] = usePersistedState(GATEWAY_MODELS_KEY, [], { listener: true, }); + const [gatewayAvailable, setGatewayAvailable] = usePersistedState( + GATEWAY_AVAILABLE_KEY, + false, + { listener: true } + ); + + // Sync gateway availability from provider config + useEffect(() => { + if (!config) return; + const available = config["mux-gateway"]?.couponCodeSet ?? false; + setGatewayAvailable(available); + }, [config, setGatewayAvailable]); const isEnabled = useCallback( (modelId: string) => gatewayModels.includes(modelId), @@ -70,5 +97,5 @@ export function useGatewayModels() { [setGatewayModels] ); - return { gatewayModels, isEnabled, toggle }; + return { gatewayModels, isEnabled, toggle, gatewayAvailable }; } From e55a878e64775f5bd876e724d6c61531de91c9d8 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 6 Dec 2025 15:54:05 -0600 Subject: [PATCH 03/16] fix: hide mux-gateway models and only show cloud for supported providers - Filter out mux-gateway provider from custom models list in settings - Filter out mux-gateway models from model LRU list - Only show cloud icon for providers that gateway supports (anthropic, openai, google) - Add isGatewaySupported() check in toGatewayModel for extra robustness --- src/browser/components/ModelSelector.tsx | 9 +++--- .../Settings/sections/ModelsSection.tsx | 21 +++++++++++--- src/browser/hooks/useGatewayModels.ts | 29 ++++++++++++++++--- src/browser/hooks/useModelLRU.ts | 2 ++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index d76565a95..831859c00 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -10,7 +10,7 @@ import { cn } from "@/common/lib/utils"; import { Cloud, Settings, Star } from "lucide-react"; import { TooltipWrapper, Tooltip } from "./Tooltip"; import { useSettings } from "@/browser/contexts/SettingsContext"; -import { useGatewayModels } from "@/browser/hooks/useGatewayModels"; +import { useGatewayModels, isGatewaySupported } from "@/browser/hooks/useGatewayModels"; interface ModelSelectorProps { value: string; @@ -196,7 +196,8 @@ export const ModelSelector = forwardRef( }, [highlightedIndex]); if (!isEditing) { - const gatewayEnabled = isGatewayEnabled(value) && gatewayAvailable; + const gatewayEnabled = + isGatewayEnabled(value) && gatewayAvailable && isGatewaySupported(value); return (
{gatewayEnabled && ( @@ -268,8 +269,8 @@ export const ModelSelector = forwardRef(
{model}
- {/* Gateway toggle - only show when gateway is configured */} - {gatewayAvailable && ( + {/* Gateway toggle - only show when gateway is configured and model's provider is supported */} + {gatewayAvailable && isGatewaySupported(model) && (
diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index 84fd86706..d74864e4f 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -5,6 +5,23 @@ import { useProvidersConfig } from "./useProvidersConfig"; const GATEWAY_MODELS_KEY = "gateway-models"; const GATEWAY_AVAILABLE_KEY = "gateway-available"; +/** + * Providers that Mux Gateway supports routing to. + * Only models from these providers can use the gateway toggle. + */ +const GATEWAY_SUPPORTED_PROVIDERS = new Set(["anthropic", "openai", "google"]); + +/** + * Check if a model's provider is supported by Mux Gateway. + * @param modelId Full model ID (e.g., "anthropic:claude-opus-4-5") + */ +export function isGatewaySupported(modelId: string): boolean { + const colonIndex = modelId.indexOf(":"); + if (colonIndex === -1) return false; + const provider = modelId.slice(0, colonIndex); + return GATEWAY_SUPPORTED_PROVIDERS.has(provider); +} + /** * Check if a model is gateway-enabled (static read, no reactivity) */ @@ -21,13 +38,17 @@ export function isGatewayAvailable(): boolean { } /** - * Transform a model ID to gateway format if gateway is enabled AND available. - * Falls back to direct provider if gateway is not configured. + * Transform a model ID to gateway format if gateway is enabled AND available AND supported. + * Falls back to direct provider if: + * - Gateway is not configured (no coupon code) + * - User hasn't enabled gateway for this model + * - Provider is not supported by gateway + * * Example: "anthropic:claude-opus-4-5" → "mux-gateway:anthropic/claude-opus-4-5" */ export function toGatewayModel(modelId: string): string { - // Only transform if user enabled gateway for this model AND gateway is configured - if (!isGatewayEnabled(modelId) || !isGatewayAvailable()) { + // Only transform if user enabled gateway for this model, gateway is configured, and provider is supported + if (!isGatewayEnabled(modelId) || !isGatewayAvailable() || !isGatewaySupported(modelId)) { return modelId; } // Transform provider:model to mux-gateway:provider/model diff --git a/src/browser/hooks/useModelLRU.ts b/src/browser/hooks/useModelLRU.ts index 06dbf2d7a..c0b198e83 100644 --- a/src/browser/hooks/useModelLRU.ts +++ b/src/browser/hooks/useModelLRU.ts @@ -85,6 +85,8 @@ export function useModelLRU() { const providerConfig = await api.providers.getConfig(); const models: string[] = []; for (const [provider, config] of Object.entries(providerConfig)) { + // Skip mux-gateway - those models are accessed via the cloud toggle, not listed separately + if (provider === "mux-gateway") continue; if (config.models) { for (const modelId of config.models) { // Format as provider:modelId for consistency From 7ded9ad1fd6fd2695b116a49be38c3f5992c7f10 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 6 Dec 2025 16:08:43 -0600 Subject: [PATCH 04/16] feat: migrate legacy mux-gateway model format to canonical form Add forward compatibility for users who have directly specified mux-gateway models in their config: - migrateGatewayModel(): converts 'mux-gateway:provider/model' to 'provider:model' and auto-enables gateway toggle for that model - Applied in useSendMessageOptions, getSendOptionsFromStorage - useModelLRU: migrates LRU list and default model on load - Malformed gateway entries are filtered out Example: 'mux-gateway:anthropic/claude-opus-4-5' becomes 'anthropic:claude-opus-4-5' with gateway toggle enabled --- src/browser/hooks/useGatewayModels.ts | 44 ++++++++++++++++++++++ src/browser/hooks/useModelLRU.ts | 17 +++++++-- src/browser/hooks/useSendMessageOptions.ts | 7 +++- src/browser/utils/messages/sendOptions.ts | 6 ++- 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index d74864e4f..0015d8860 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -30,6 +30,50 @@ export function isGatewayEnabled(modelId: string): boolean { return gatewayModels.includes(modelId); } +/** + * Check if a model string is in mux-gateway format. + * @param modelId Model string to check + * @returns true if model is "mux-gateway:provider/model" format + */ +export function isGatewayFormat(modelId: string): boolean { + return modelId.startsWith("mux-gateway:"); +} + +/** + * Migrate a mux-gateway model to canonical format and enable gateway toggle. + * Converts "mux-gateway:provider/model" to "provider:model" and marks it for gateway routing. + * + * This provides forward compatibility for users who have directly specified + * mux-gateway models in their config. + * + * @param modelId Model string that may be in gateway format + * @returns Canonical model ID (e.g., "anthropic:claude-opus-4-5") + */ +export function migrateGatewayModel(modelId: string): string { + if (!isGatewayFormat(modelId)) { + return modelId; + } + + // mux-gateway:anthropic/claude-opus-4-5 → anthropic:claude-opus-4-5 + const inner = modelId.slice("mux-gateway:".length); + const slashIndex = inner.indexOf("/"); + if (slashIndex === -1) { + return modelId; // Malformed, return as-is + } + + const provider = inner.slice(0, slashIndex); + const model = inner.slice(slashIndex + 1); + const canonicalId = `${provider}:${model}`; + + // Auto-enable gateway for this model (one-time migration) + const gatewayModels = readPersistedState(GATEWAY_MODELS_KEY, []); + if (!gatewayModels.includes(canonicalId)) { + updatePersistedState(GATEWAY_MODELS_KEY, [...gatewayModels, canonicalId]); + } + + return canonicalId; +} + /** * Check if the gateway provider is available (has coupon code configured) */ diff --git a/src/browser/hooks/useModelLRU.ts b/src/browser/hooks/useModelLRU.ts index c0b198e83..898e07176 100644 --- a/src/browser/hooks/useModelLRU.ts +++ b/src/browser/hooks/useModelLRU.ts @@ -4,6 +4,7 @@ import { MODEL_ABBREVIATIONS } from "@/browser/utils/slashCommands/registry"; import { defaultModel } from "@/common/utils/ai/models"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { useAPI } from "@/browser/contexts/API"; +import { migrateGatewayModel, isGatewayFormat } from "./useGatewayModels"; const MAX_LRU_SIZE = 12; const LRU_KEY = "model-lru"; @@ -36,7 +37,9 @@ export function evictModelFromLRU(model: string): void { export function getDefaultModel(): string { const persisted = readPersistedState(DEFAULT_MODEL_KEY, null); - return persisted ?? FALLBACK_MODEL; + if (!persisted) return FALLBACK_MODEL; + // Migrate legacy mux-gateway format to canonical form + return migrateGatewayModel(persisted); } /** @@ -60,10 +63,18 @@ export function useModelLRU() { { listener: true } ); - // Merge any new defaults from MODEL_ABBREVIATIONS (only once on mount) + // Merge any new defaults from MODEL_ABBREVIATIONS and migrate legacy gateway models (only once on mount) useEffect(() => { setRecentModels((prev) => { - const merged = [...prev]; + // Migrate any mux-gateway:provider/model entries to canonical form + const migrated = prev.map((m) => migrateGatewayModel(m)); + // Remove any remaining mux-gateway entries that couldn't be migrated + const filtered = migrated.filter((m) => !isGatewayFormat(m)); + // Deduplicate (migration might create duplicates) + const deduped = [...new Set(filtered)]; + + // Merge defaults + const merged = [...deduped]; for (const defaultModel of DEFAULT_MODELS) { if (!merged.includes(defaultModel)) { merged.push(defaultModel); diff --git a/src/browser/hooks/useSendMessageOptions.ts b/src/browser/hooks/useSendMessageOptions.ts index 39bcb3e13..63c12f725 100644 --- a/src/browser/hooks/useSendMessageOptions.ts +++ b/src/browser/hooks/useSendMessageOptions.ts @@ -2,7 +2,7 @@ import { useThinkingLevel } from "./useThinkingLevel"; import { useMode } from "@/browser/contexts/ModeContext"; import { usePersistedState } from "./usePersistedState"; import { getDefaultModel } from "./useModelLRU"; -import { toGatewayModel } from "./useGatewayModels"; +import { toGatewayModel, migrateGatewayModel } from "./useGatewayModels"; import { modeToToolPolicy, PLAN_MODE_INSTRUCTION } from "@/common/utils/ui/modeUtils"; import { getModelKey } from "@/common/constants/storage"; import type { SendMessageOptions } from "@/common/orpc/types"; @@ -27,9 +27,12 @@ function constructSendMessageOptions( const additionalSystemInstructions = mode === "plan" ? PLAN_MODE_INSTRUCTION : undefined; // Ensure model is always a valid string (defensive against corrupted localStorage) - const baseModel = + const rawModel = typeof preferredModel === "string" && preferredModel ? preferredModel : fallbackModel; + // Migrate any legacy mux-gateway:provider/model format to canonical form + const baseModel = migrateGatewayModel(rawModel); + // Transform to gateway format if gateway is enabled for this model const model = toGatewayModel(baseModel); diff --git a/src/browser/utils/messages/sendOptions.ts b/src/browser/utils/messages/sendOptions.ts index 5573297ad..646643efa 100644 --- a/src/browser/utils/messages/sendOptions.ts +++ b/src/browser/utils/messages/sendOptions.ts @@ -2,7 +2,7 @@ import { getModelKey, getThinkingLevelKey, getModeKey } from "@/common/constants import { modeToToolPolicy, PLAN_MODE_INSTRUCTION } from "@/common/utils/ui/modeUtils"; import { readPersistedState } from "@/browser/hooks/usePersistedState"; import { getDefaultModel } from "@/browser/hooks/useModelLRU"; -import { toGatewayModel } from "@/browser/hooks/useGatewayModels"; +import { toGatewayModel, migrateGatewayModel } from "@/browser/hooks/useGatewayModels"; import type { SendMessageOptions } from "@/common/orpc/types"; import type { UIMode } from "@/common/types/mode"; import type { ThinkingLevel } from "@/common/types/thinking"; @@ -39,7 +39,9 @@ function getProviderOptions(): MuxProviderOptions { */ export function getSendOptionsFromStorage(workspaceId: string): SendMessageOptions { // Read model preference (workspace-specific), fallback to LRU default - const baseModel = readPersistedState(getModelKey(workspaceId), getDefaultModel()); + const rawModel = readPersistedState(getModelKey(workspaceId), getDefaultModel()); + // Migrate any legacy mux-gateway:provider/model format to canonical form + const baseModel = migrateGatewayModel(rawModel); // Transform to gateway format if gateway is enabled for this model const model = toGatewayModel(baseModel); From b154f76ee2c4fd995fae4a4d3dc24bd72060c002 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 10:30:07 -0600 Subject: [PATCH 05/16] fix: expand gateway supported providers based on Vercel AI Gateway docs Add xai and bedrock to supported providers list. The Vercel AI Gateway supports OpenAI, Anthropic, Google, xAI, Amazon Bedrock, and others. Excluded providers: - ollama: Local-only, not routable through cloud gateway - openrouter: Already a gateway/aggregator --- src/browser/hooks/useGatewayModels.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index 0015d8860..0984a6edd 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -7,9 +7,15 @@ const GATEWAY_AVAILABLE_KEY = "gateway-available"; /** * Providers that Mux Gateway supports routing to. + * Based on Vercel AI Gateway supported providers. * Only models from these providers can use the gateway toggle. + * + * Excluded: + * - ollama: Local-only provider, not routable through cloud gateway + * - openrouter: Already a gateway/aggregator, routing through another gateway is redundant + * - mux-gateway: Already gateway format */ -const GATEWAY_SUPPORTED_PROVIDERS = new Set(["anthropic", "openai", "google"]); +const GATEWAY_SUPPORTED_PROVIDERS = new Set(["anthropic", "openai", "google", "xai", "bedrock"]); /** * Check if a model's provider is supported by Mux Gateway. From c655e18d086ede8a6572bcd6cbc41006c934bbac Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 10:35:14 -0600 Subject: [PATCH 06/16] refactor: simplify gateway to anthropic, openai, google, xai only Remove bedrock from supported gateway providers due to its complex AWS credential-based authentication, which doesn't fit the simple API key routing model of the gateway. --- src/browser/hooks/useGatewayModels.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index 0984a6edd..5925d8b1a 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -13,9 +13,10 @@ const GATEWAY_AVAILABLE_KEY = "gateway-available"; * Excluded: * - ollama: Local-only provider, not routable through cloud gateway * - openrouter: Already a gateway/aggregator, routing through another gateway is redundant + * - bedrock: Complex auth (AWS credentials), not simple API key routing * - mux-gateway: Already gateway format */ -const GATEWAY_SUPPORTED_PROVIDERS = new Set(["anthropic", "openai", "google", "xai", "bedrock"]); +const GATEWAY_SUPPORTED_PROVIDERS = new Set(["anthropic", "openai", "google", "xai"]); /** * Check if a model's provider is supported by Mux Gateway. From d75244fd5174d31c0d2d4ad327f39b5af592ad54 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 10:58:54 -0600 Subject: [PATCH 07/16] feat: add custom GatewayIcon for model gateway indicator Replace generic Cloud icon with a dedicated gateway icon showing data flowing through a central hub (hexagon with arrows). More distinctive and semantically meaningful for the gateway feature. --- src/browser/components/ModelSelector.tsx | 7 ++-- .../components/Settings/sections/ModelRow.tsx | 7 ++-- src/browser/components/icons/GatewayIcon.tsx | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/browser/components/icons/GatewayIcon.tsx diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index 831859c00..3a0594a4d 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -7,7 +7,8 @@ import React, { forwardRef, } from "react"; import { cn } from "@/common/lib/utils"; -import { Cloud, Settings, Star } from "lucide-react"; +import { Settings, Star } from "lucide-react"; +import { GatewayIcon } from "./icons/GatewayIcon"; import { TooltipWrapper, Tooltip } from "./Tooltip"; import { useSettings } from "@/browser/contexts/SettingsContext"; import { useGatewayModels, isGatewaySupported } from "@/browser/hooks/useGatewayModels"; @@ -202,7 +203,7 @@ export const ModelSelector = forwardRef(
{gatewayEnabled && ( - + Using Mux Gateway @@ -290,7 +291,7 @@ export const ModelSelector = forwardRef( isGatewayEnabled(model) ? "Disable Mux Gateway" : "Enable Mux Gateway" } > - diff --git a/src/browser/components/Settings/sections/ModelRow.tsx b/src/browser/components/Settings/sections/ModelRow.tsx index 77505a0c5..27d612964 100644 --- a/src/browser/components/Settings/sections/ModelRow.tsx +++ b/src/browser/components/Settings/sections/ModelRow.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { Check, Cloud, Pencil, Star, Trash2, X } from "lucide-react"; +import { Check, Pencil, Star, Trash2, X } from "lucide-react"; +import { GatewayIcon } from "@/browser/components/icons/GatewayIcon"; import { cn } from "@/common/lib/utils"; import { TooltipWrapper, Tooltip } from "@/browser/components/Tooltip"; import { ProviderWithIcon } from "@/browser/components/ProviderIcon"; @@ -106,7 +107,9 @@ export function ModelRow(props: ModelRowProps) { )} aria-label={props.isGatewayEnabled ? "Disable Mux Gateway" : "Enable Mux Gateway"} > - + {props.isGatewayEnabled ? "Using Mux Gateway" : "Use Mux Gateway"} diff --git a/src/browser/components/icons/GatewayIcon.tsx b/src/browser/components/icons/GatewayIcon.tsx new file mode 100644 index 000000000..2442146aa --- /dev/null +++ b/src/browser/components/icons/GatewayIcon.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +interface GatewayIconProps extends React.SVGProps { + className?: string; +} + +/** + * Gateway icon - represents routing through Mux Gateway. + * A stylized "relay" symbol showing data passing through a central hub. + */ +export function GatewayIcon(props: GatewayIconProps) { + return ( + + {/* Central hexagon hub */} + + {/* Left incoming arrow */} + + + {/* Right outgoing arrow */} + + + + ); +} From 805fdca331e5c6a07ac88ef0b5f49dc54f9fff01 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:06:09 -0600 Subject: [PATCH 08/16] feat: add gateway enabled toggle in Providers settings - Add global gateway enabled state to localStorage (defaults to true) - Add toggle switch in Mux Gateway provider section when coupon is set - Respect global enabled state across all gateway UI elements - Simplify GatewayIcon to diamond relay symbol This allows users to disable gateway routing without removing their coupon code, useful for testing or debugging. --- src/browser/components/ModelSelector.tsx | 10 ++-- .../Settings/sections/ModelsSection.tsx | 5 +- .../Settings/sections/ProvidersSection.tsx | 27 +++++++++++ src/browser/components/icons/GatewayIcon.tsx | 17 ++++--- src/browser/hooks/useGatewayModels.ts | 46 +++++++++++++++++-- 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index 3a0594a4d..83106bdf3 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -33,6 +33,7 @@ export const ModelSelector = forwardRef( isEnabled: isGatewayEnabled, toggle: toggleGateway, gatewayAvailable, + gatewayGloballyEnabled, } = useGatewayModels(); const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(value); @@ -198,7 +199,10 @@ export const ModelSelector = forwardRef( if (!isEditing) { const gatewayEnabled = - isGatewayEnabled(value) && gatewayAvailable && isGatewaySupported(value); + gatewayGloballyEnabled && + isGatewayEnabled(value) && + gatewayAvailable && + isGatewaySupported(value); return (
{gatewayEnabled && ( @@ -270,8 +274,8 @@ export const ModelSelector = forwardRef(
{model}
- {/* Gateway toggle - only show when gateway is configured and model's provider is supported */} - {gatewayAvailable && isGatewaySupported(model) && ( + {/* Gateway toggle - only show when gateway is globally enabled, configured, and model's provider is supported */} + {gatewayGloballyEnabled && gatewayAvailable && isGatewaySupported(model) && (
); })} + + {/* Gateway enabled toggle - only for mux-gateway when configured */} + {provider === "mux-gateway" && gatewayAvailable && ( +
+
+ + Route requests through Mux Gateway +
+ +
+ )}
)}
diff --git a/src/browser/components/icons/GatewayIcon.tsx b/src/browser/components/icons/GatewayIcon.tsx index 2442146aa..c34d925d2 100644 --- a/src/browser/components/icons/GatewayIcon.tsx +++ b/src/browser/components/icons/GatewayIcon.tsx @@ -6,7 +6,7 @@ interface GatewayIconProps extends React.SVGProps { /** * Gateway icon - represents routing through Mux Gateway. - * A stylized "relay" symbol showing data passing through a central hub. + * A simplified relay symbol: arrow passing through a central node. */ export function GatewayIcon(props: GatewayIconProps) { return ( @@ -20,14 +20,13 @@ export function GatewayIcon(props: GatewayIconProps) { strokeLinejoin="round" {...props} > - {/* Central hexagon hub */} - - {/* Left incoming arrow */} - - - {/* Right outgoing arrow */} - - + {/* Central diamond/node */} + + {/* Input line */} + + {/* Output arrow */} + + ); } diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index 5925d8b1a..663e3c809 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -4,6 +4,7 @@ import { useProvidersConfig } from "./useProvidersConfig"; const GATEWAY_MODELS_KEY = "gateway-models"; const GATEWAY_AVAILABLE_KEY = "gateway-available"; +const GATEWAY_ENABLED_KEY = "gateway-enabled"; /** * Providers that Mux Gateway supports routing to. @@ -88,6 +89,20 @@ export function isGatewayAvailable(): boolean { return readPersistedState(GATEWAY_AVAILABLE_KEY, false); } +/** + * Check if the gateway is globally enabled (user toggle, defaults to true when available) + */ +export function isGatewayGloballyEnabled(): boolean { + return readPersistedState(GATEWAY_ENABLED_KEY, true); +} + +/** + * Set the global gateway enabled state + */ +export function setGatewayGloballyEnabled(enabled: boolean): void { + updatePersistedState(GATEWAY_ENABLED_KEY, enabled); +} + /** * Transform a model ID to gateway format if gateway is enabled AND available AND supported. * Falls back to direct provider if: @@ -98,8 +113,17 @@ export function isGatewayAvailable(): boolean { * Example: "anthropic:claude-opus-4-5" → "mux-gateway:anthropic/claude-opus-4-5" */ export function toGatewayModel(modelId: string): string { - // Only transform if user enabled gateway for this model, gateway is configured, and provider is supported - if (!isGatewayEnabled(modelId) || !isGatewayAvailable() || !isGatewaySupported(modelId)) { + // Only transform if: + // 1. Gateway is globally enabled (user hasn't disabled it) + // 2. User enabled gateway for this specific model + // 3. Gateway is configured (coupon code set) + // 4. Provider is supported by gateway + if ( + !isGatewayGloballyEnabled() || + !isGatewayEnabled(modelId) || + !isGatewayAvailable() || + !isGatewaySupported(modelId) + ) { return modelId; } // Transform provider:model to mux-gateway:provider/model @@ -144,6 +168,11 @@ export function useGatewayModels() { false, { listener: true } ); + const [gatewayGloballyEnabled, setGatewayGloballyEnabled] = usePersistedState( + GATEWAY_ENABLED_KEY, + true, + { listener: true } + ); // Sync gateway availability from provider config useEffect(() => { @@ -152,6 +181,10 @@ export function useGatewayModels() { setGatewayAvailable(available); }, [config, setGatewayAvailable]); + const toggleGloballyEnabled = useCallback(() => { + setGatewayGloballyEnabled((prev) => !prev); + }, [setGatewayGloballyEnabled]); + const isEnabled = useCallback( (modelId: string) => gatewayModels.includes(modelId), [gatewayModels] @@ -169,5 +202,12 @@ export function useGatewayModels() { [setGatewayModels] ); - return { gatewayModels, isEnabled, toggle, gatewayAvailable }; + return { + gatewayModels, + isEnabled, + toggle, + gatewayAvailable, + gatewayGloballyEnabled, + toggleGloballyEnabled, + }; } From 9392cad2b1e488ec12d01914c3c3e1e8706fe56e Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:09:53 -0600 Subject: [PATCH 09/16] refactor: simplify gateway API with clearer naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename useGatewayModels → useGateway with cleaner interface: - isActive: gateway configured AND enabled (ready to use) - isConfigured: coupon code set - isEnabled: global master switch on - canToggleModel(id): whether to show gateway toggle for model - modelUsesGateway(id): model is toggled for gateway - isModelRoutingThroughGateway(id): active + uses (for display) - toggleEnabled(): toggle global switch - toggleModelGateway(id): toggle per-model Consolidates scattered checks like: gatewayGloballyEnabled && gatewayAvailable && isGatewaySupported(model) Into single semantic calls: gateway.canToggleModel(model) Also renames isGatewaySupported → isProviderSupported for clarity. --- src/browser/components/ModelSelector.tsx | 38 ++- .../Settings/sections/ModelsSection.tsx | 21 +- .../Settings/sections/ProvidersSection.tsx | 14 +- src/browser/hooks/useGatewayModels.ts | 233 +++++++++--------- 4 files changed, 152 insertions(+), 154 deletions(-) diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index 83106bdf3..3ed9b0e5c 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -11,7 +11,7 @@ import { Settings, Star } from "lucide-react"; import { GatewayIcon } from "./icons/GatewayIcon"; import { TooltipWrapper, Tooltip } from "./Tooltip"; import { useSettings } from "@/browser/contexts/SettingsContext"; -import { useGatewayModels, isGatewaySupported } from "@/browser/hooks/useGatewayModels"; +import { useGateway } from "@/browser/hooks/useGatewayModels"; interface ModelSelectorProps { value: string; @@ -29,12 +29,7 @@ export interface ModelSelectorRef { export const ModelSelector = forwardRef( ({ value, onChange, recentModels, onComplete, defaultModel, onSetDefaultModel }, ref) => { const { open: openSettings } = useSettings(); - const { - isEnabled: isGatewayEnabled, - toggle: toggleGateway, - gatewayAvailable, - gatewayGloballyEnabled, - } = useGatewayModels(); + const gateway = useGateway(); const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(value); const [error, setError] = useState(null); @@ -198,14 +193,10 @@ export const ModelSelector = forwardRef( }, [highlightedIndex]); if (!isEditing) { - const gatewayEnabled = - gatewayGloballyEnabled && - isGatewayEnabled(value) && - gatewayAvailable && - isGatewaySupported(value); + const gatewayActive = gateway.isModelRoutingThroughGateway(value); return (
- {gatewayEnabled && ( + {gatewayActive && ( @@ -274,8 +265,8 @@ export const ModelSelector = forwardRef(
{model}
- {/* Gateway toggle - only show when gateway is globally enabled, configured, and model's provider is supported */} - {gatewayGloballyEnabled && gatewayAvailable && isGatewaySupported(model) && ( + {/* Gateway toggle */} + {gateway.canToggleModel(model) && ( - {isGatewayEnabled(model) ? "Using Mux Gateway" : "Use Mux Gateway"} + {gateway.modelUsesGateway(model) + ? "Using Mux Gateway" + : "Use Mux Gateway"} )} diff --git a/src/browser/components/Settings/sections/ModelsSection.tsx b/src/browser/components/Settings/sections/ModelsSection.tsx index 7d8fe371e..b255e60ef 100644 --- a/src/browser/components/Settings/sections/ModelsSection.tsx +++ b/src/browser/components/Settings/sections/ModelsSection.tsx @@ -3,7 +3,7 @@ import { Plus, Loader2 } from "lucide-react"; import { SUPPORTED_PROVIDERS, PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; import { useModelLRU } from "@/browser/hooks/useModelLRU"; -import { useGatewayModels, isGatewaySupported } from "@/browser/hooks/useGatewayModels"; +import { useGateway } from "@/browser/hooks/useGatewayModels"; import { ModelRow } from "./ModelRow"; import { useAPI } from "@/browser/contexts/API"; import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; @@ -29,12 +29,7 @@ export function ModelsSection() { const [editing, setEditing] = useState(null); const [error, setError] = useState(null); const { defaultModel, setDefaultModel } = useModelLRU(); - const { - isEnabled: isGatewayEnabled, - toggle: toggleGateway, - gatewayAvailable, - gatewayGloballyEnabled, - } = useGatewayModels(); + const gateway = useGateway(); // Check if a model already exists (for duplicate prevention) const modelExists = useCallback( @@ -225,7 +220,7 @@ export function ModelsSection() { editError={isModelEditing ? error : undefined} saving={false} hasActiveEdit={editing !== null} - isGatewayEnabled={isGatewayEnabled(model.fullId)} + isGatewayEnabled={gateway.modelUsesGateway(model.fullId)} onSetDefault={() => setDefaultModel(model.fullId)} onStartEdit={() => handleStartEdit(model.provider, model.modelId)} onSaveEdit={handleSaveEdit} @@ -235,8 +230,8 @@ export function ModelsSection() { } onRemove={() => handleRemoveModel(model.provider, model.modelId)} onToggleGateway={ - gatewayGloballyEnabled && gatewayAvailable && isGatewaySupported(model.fullId) - ? () => toggleGateway(model.fullId) + gateway.canToggleModel(model.fullId) + ? () => gateway.toggleModelGateway(model.fullId) : undefined } /> @@ -259,11 +254,11 @@ export function ModelsSection() { isCustom={false} isDefault={defaultModel === model.fullId} isEditing={false} - isGatewayEnabled={isGatewayEnabled(model.fullId)} + isGatewayEnabled={gateway.modelUsesGateway(model.fullId)} onSetDefault={() => setDefaultModel(model.fullId)} onToggleGateway={ - gatewayGloballyEnabled && gatewayAvailable && isGatewaySupported(model.fullId) - ? () => toggleGateway(model.fullId) + gateway.canToggleModel(model.fullId) + ? () => gateway.toggleModelGateway(model.fullId) : undefined } /> diff --git a/src/browser/components/Settings/sections/ProvidersSection.tsx b/src/browser/components/Settings/sections/ProvidersSection.tsx index 1687dd508..834f80abc 100644 --- a/src/browser/components/Settings/sections/ProvidersSection.tsx +++ b/src/browser/components/Settings/sections/ProvidersSection.tsx @@ -5,7 +5,7 @@ import type { ProviderName } from "@/common/constants/providers"; import { ProviderWithIcon } from "@/browser/components/ProviderIcon"; import { useAPI } from "@/browser/contexts/API"; import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; -import { useGatewayModels } from "@/browser/hooks/useGatewayModels"; +import { useGateway } from "@/browser/hooks/useGatewayModels"; interface FieldConfig { key: string; @@ -70,7 +70,7 @@ function getProviderFields(provider: ProviderName): FieldConfig[] { export function ProvidersSection() { const { api } = useAPI(); const { config, updateOptimistically } = useProvidersConfig(); - const { gatewayAvailable, gatewayGloballyEnabled, toggleGloballyEnabled } = useGatewayModels(); + const gateway = useGateway(); const [expandedProvider, setExpandedProvider] = useState(null); const [editingField, setEditingField] = useState<{ provider: string; @@ -319,7 +319,7 @@ export function ProvidersSection() { })} {/* Gateway enabled toggle - only for mux-gateway when configured */} - {provider === "mux-gateway" && gatewayAvailable && ( + {provider === "mux-gateway" && gateway.isConfigured && (
@@ -327,16 +327,16 @@ export function ProvidersSection() {
diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index 663e3c809..39d36c2ae 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -1,15 +1,15 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { usePersistedState, readPersistedState, updatePersistedState } from "./usePersistedState"; import { useProvidersConfig } from "./useProvidersConfig"; -const GATEWAY_MODELS_KEY = "gateway-models"; -const GATEWAY_AVAILABLE_KEY = "gateway-available"; -const GATEWAY_ENABLED_KEY = "gateway-enabled"; +// localStorage keys +const GATEWAY_MODELS_KEY = "gateway-models"; // Models user has enabled for gateway routing +const GATEWAY_CONFIGURED_KEY = "gateway-available"; // Synced from provider config (coupon set) +const GATEWAY_ENABLED_KEY = "gateway-enabled"; // Global on/off toggle /** * Providers that Mux Gateway supports routing to. * Based on Vercel AI Gateway supported providers. - * Only models from these providers can use the gateway toggle. * * Excluded: * - ollama: Local-only provider, not routable through cloud gateway @@ -19,29 +19,28 @@ const GATEWAY_ENABLED_KEY = "gateway-enabled"; */ const GATEWAY_SUPPORTED_PROVIDERS = new Set(["anthropic", "openai", "google", "xai"]); +// ============================================================================ +// Pure utility functions (no side effects, used for message sending) +// ============================================================================ + /** - * Check if a model's provider is supported by Mux Gateway. - * @param modelId Full model ID (e.g., "anthropic:claude-opus-4-5") + * Extract provider from a model ID. */ -export function isGatewaySupported(modelId: string): boolean { +function getProvider(modelId: string): string | null { const colonIndex = modelId.indexOf(":"); - if (colonIndex === -1) return false; - const provider = modelId.slice(0, colonIndex); - return GATEWAY_SUPPORTED_PROVIDERS.has(provider); + return colonIndex === -1 ? null : modelId.slice(0, colonIndex); } /** - * Check if a model is gateway-enabled (static read, no reactivity) + * Check if a model's provider can route through Mux Gateway. */ -export function isGatewayEnabled(modelId: string): boolean { - const gatewayModels = readPersistedState(GATEWAY_MODELS_KEY, []); - return gatewayModels.includes(modelId); +export function isProviderSupported(modelId: string): boolean { + const provider = getProvider(modelId); + return provider !== null && GATEWAY_SUPPORTED_PROVIDERS.has(provider); } /** * Check if a model string is in mux-gateway format. - * @param modelId Model string to check - * @returns true if model is "mux-gateway:provider/model" format */ export function isGatewayFormat(modelId: string): boolean { return modelId.startsWith("mux-gateway:"); @@ -53,9 +52,6 @@ export function isGatewayFormat(modelId: string): boolean { * * This provides forward compatibility for users who have directly specified * mux-gateway models in their config. - * - * @param modelId Model string that may be in gateway format - * @returns Canonical model ID (e.g., "anthropic:claude-opus-4-5") */ export function migrateGatewayModel(modelId: string): string { if (!isGatewayFormat(modelId)) { @@ -83,131 +79,140 @@ export function migrateGatewayModel(modelId: string): string { } /** - * Check if the gateway provider is available (has coupon code configured) - */ -export function isGatewayAvailable(): boolean { - return readPersistedState(GATEWAY_AVAILABLE_KEY, false); -} - -/** - * Check if the gateway is globally enabled (user toggle, defaults to true when available) - */ -export function isGatewayGloballyEnabled(): boolean { - return readPersistedState(GATEWAY_ENABLED_KEY, true); -} - -/** - * Set the global gateway enabled state - */ -export function setGatewayGloballyEnabled(enabled: boolean): void { - updatePersistedState(GATEWAY_ENABLED_KEY, enabled); -} - -/** - * Transform a model ID to gateway format if gateway is enabled AND available AND supported. - * Falls back to direct provider if: - * - Gateway is not configured (no coupon code) - * - User hasn't enabled gateway for this model - * - Provider is not supported by gateway + * Transform a model ID to gateway format for API calls. + * Returns original modelId if gateway routing shouldn't be used. + * + * Checks (all must pass): + * 1. Gateway is globally enabled (user hasn't disabled it) + * 2. Gateway is configured (coupon code set) + * 3. Provider is supported by gateway + * 4. User enabled gateway for this specific model * * Example: "anthropic:claude-opus-4-5" → "mux-gateway:anthropic/claude-opus-4-5" */ export function toGatewayModel(modelId: string): string { - // Only transform if: - // 1. Gateway is globally enabled (user hasn't disabled it) - // 2. User enabled gateway for this specific model - // 3. Gateway is configured (coupon code set) - // 4. Provider is supported by gateway - if ( - !isGatewayGloballyEnabled() || - !isGatewayEnabled(modelId) || - !isGatewayAvailable() || - !isGatewaySupported(modelId) - ) { + const globallyEnabled = readPersistedState(GATEWAY_ENABLED_KEY, true); + const configured = readPersistedState(GATEWAY_CONFIGURED_KEY, false); + const enabledModels = readPersistedState(GATEWAY_MODELS_KEY, []); + + if (!globallyEnabled || !configured || !isProviderSupported(modelId)) { return modelId; } - // Transform provider:model to mux-gateway:provider/model - const colonIndex = modelId.indexOf(":"); - if (colonIndex === -1) { + + if (!enabledModels.includes(modelId)) { return modelId; } - const provider = modelId.slice(0, colonIndex); - const model = modelId.slice(colonIndex + 1); + + // Transform provider:model to mux-gateway:provider/model + const provider = getProvider(modelId); + if (!provider) return modelId; + + const model = modelId.slice(provider.length + 1); return `mux-gateway:${provider}/${model}`; } -/** - * Toggle gateway mode for a model (static update, no reactivity) - */ -export function toggleGatewayModel(modelId: string): void { - const gatewayModels = readPersistedState(GATEWAY_MODELS_KEY, []); - if (gatewayModels.includes(modelId)) { - updatePersistedState( - GATEWAY_MODELS_KEY, - gatewayModels.filter((m) => m !== modelId) - ); - } else { - updatePersistedState(GATEWAY_MODELS_KEY, [...gatewayModels, modelId]); - } +// ============================================================================ +// Gateway state interface (returned by hook) +// ============================================================================ + +export interface GatewayState { + /** Gateway is configured (coupon code set) and globally enabled */ + isActive: boolean; + /** Gateway has coupon code configured */ + isConfigured: boolean; + /** Gateway is globally enabled (master switch) */ + isEnabled: boolean; + /** Toggle the global enabled state */ + toggleEnabled: () => void; + /** Check if a specific model uses gateway routing */ + modelUsesGateway: (modelId: string) => boolean; + /** Toggle gateway routing for a specific model */ + toggleModelGateway: (modelId: string) => void; + /** Check if gateway toggle should be shown for a model (active + provider supported) */ + canToggleModel: (modelId: string) => boolean; + /** Check if model is actively routing through gateway (for display) */ + isModelRoutingThroughGateway: (modelId: string) => boolean; } /** - * Hook to manage which models use the Mux Gateway. - * Returns reactive state and toggle function. + * Hook for gateway state management. * - * Also syncs gateway availability from provider config to localStorage - * so that toGatewayModel() can check it synchronously. + * Syncs gateway configuration from provider config to localStorage + * so that toGatewayModel() can check it synchronously during message sending. */ -export function useGatewayModels() { +export function useGateway(): GatewayState { const { config } = useProvidersConfig(); - const [gatewayModels, setGatewayModels] = usePersistedState(GATEWAY_MODELS_KEY, [], { + + const [enabledModels, setEnabledModels] = usePersistedState(GATEWAY_MODELS_KEY, [], { listener: true, }); - const [gatewayAvailable, setGatewayAvailable] = usePersistedState( - GATEWAY_AVAILABLE_KEY, + const [isConfigured, setIsConfigured] = usePersistedState( + GATEWAY_CONFIGURED_KEY, false, { listener: true } ); - const [gatewayGloballyEnabled, setGatewayGloballyEnabled] = usePersistedState( - GATEWAY_ENABLED_KEY, - true, - { listener: true } - ); + const [isEnabled, setIsEnabled] = usePersistedState(GATEWAY_ENABLED_KEY, true, { + listener: true, + }); - // Sync gateway availability from provider config + // Sync gateway configuration from provider config useEffect(() => { if (!config) return; - const available = config["mux-gateway"]?.couponCodeSet ?? false; - setGatewayAvailable(available); - }, [config, setGatewayAvailable]); + const configured = config["mux-gateway"]?.couponCodeSet ?? false; + setIsConfigured(configured); + }, [config, setIsConfigured]); - const toggleGloballyEnabled = useCallback(() => { - setGatewayGloballyEnabled((prev) => !prev); - }, [setGatewayGloballyEnabled]); + const isActive = isConfigured && isEnabled; - const isEnabled = useCallback( - (modelId: string) => gatewayModels.includes(modelId), - [gatewayModels] + const toggleEnabled = useCallback(() => { + setIsEnabled((prev) => !prev); + }, [setIsEnabled]); + + const modelUsesGateway = useCallback( + (modelId: string) => enabledModels.includes(modelId), + [enabledModels] ); - const toggle = useCallback( + const toggleModelGateway = useCallback( (modelId: string) => { - setGatewayModels((prev) => { - if (prev.includes(modelId)) { - return prev.filter((m) => m !== modelId); - } - return [...prev, modelId]; - }); + setEnabledModels((prev) => + prev.includes(modelId) ? prev.filter((m) => m !== modelId) : [...prev, modelId] + ); }, - [setGatewayModels] + [setEnabledModels] ); - return { - gatewayModels, - isEnabled, - toggle, - gatewayAvailable, - gatewayGloballyEnabled, - toggleGloballyEnabled, - }; + const canToggleModel = useCallback( + (modelId: string) => isActive && isProviderSupported(modelId), + [isActive] + ); + + const isModelRoutingThroughGateway = useCallback( + (modelId: string) => + isActive && isProviderSupported(modelId) && enabledModels.includes(modelId), + [isActive, enabledModels] + ); + + return useMemo( + () => ({ + isActive, + isConfigured, + isEnabled, + toggleEnabled, + modelUsesGateway, + toggleModelGateway, + canToggleModel, + isModelRoutingThroughGateway, + }), + [ + isActive, + isConfigured, + isEnabled, + toggleEnabled, + modelUsesGateway, + toggleModelGateway, + canToggleModel, + isModelRoutingThroughGateway, + ] + ); } From 0b20bc3dbcac97e1a42f4d4320caafb3e337ca07 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:26:35 -0600 Subject: [PATCH 10/16] style: redesign gateway icon as circular portal Use a circle (portal) with an arrow passing through instead of a diamond relay. More visually balanced at small sizes. --- src/browser/components/icons/GatewayIcon.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/browser/components/icons/GatewayIcon.tsx b/src/browser/components/icons/GatewayIcon.tsx index c34d925d2..a105a4f91 100644 --- a/src/browser/components/icons/GatewayIcon.tsx +++ b/src/browser/components/icons/GatewayIcon.tsx @@ -6,7 +6,7 @@ interface GatewayIconProps extends React.SVGProps { /** * Gateway icon - represents routing through Mux Gateway. - * A simplified relay symbol: arrow passing through a central node. + * A portal symbol: circle with an arrow passing through. */ export function GatewayIcon(props: GatewayIconProps) { return ( @@ -20,13 +20,11 @@ export function GatewayIcon(props: GatewayIconProps) { strokeLinejoin="round" {...props} > - {/* Central diamond/node */} - - {/* Input line */} - - {/* Output arrow */} - - + {/* Portal circle */} + + {/* Arrow passing through */} + + ); } From 1b7cb46b004b6b99508afcfcea0ac58746fbd3de Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:27:10 -0600 Subject: [PATCH 11/16] style: make gateway portal icon more mystic Concentric rings with a sparkle/cross at center for a more magical portal appearance. --- src/browser/components/icons/GatewayIcon.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/browser/components/icons/GatewayIcon.tsx b/src/browser/components/icons/GatewayIcon.tsx index a105a4f91..e634dcfdd 100644 --- a/src/browser/components/icons/GatewayIcon.tsx +++ b/src/browser/components/icons/GatewayIcon.tsx @@ -6,7 +6,7 @@ interface GatewayIconProps extends React.SVGProps { /** * Gateway icon - represents routing through Mux Gateway. - * A portal symbol: circle with an arrow passing through. + * A mystic portal: concentric rings with a sparkle at center. */ export function GatewayIcon(props: GatewayIconProps) { return ( @@ -20,11 +20,13 @@ export function GatewayIcon(props: GatewayIconProps) { strokeLinejoin="round" {...props} > - {/* Portal circle */} - - {/* Arrow passing through */} - - + {/* Outer ring */} + + {/* Inner ring */} + + {/* Center sparkle */} + + ); } From 9b4ad540e8d8dd5f3382faa0e3d77c07c063361a Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:30:57 -0600 Subject: [PATCH 12/16] style: gateway icon as M in circle with active state - Circle with M logo for clear Mux branding - Active state shows outer glow ring instead of fill - Cleaner visual distinction between enabled/disabled --- src/browser/components/ModelSelector.tsx | 8 +++---- .../components/Settings/sections/ModelRow.tsx | 4 +--- src/browser/components/icons/GatewayIcon.tsx | 21 +++++++++++-------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/browser/components/ModelSelector.tsx b/src/browser/components/ModelSelector.tsx index 3ed9b0e5c..8654b75b6 100644 --- a/src/browser/components/ModelSelector.tsx +++ b/src/browser/components/ModelSelector.tsx @@ -198,7 +198,7 @@ export const ModelSelector = forwardRef(
{gatewayActive && ( - + Using Mux Gateway @@ -289,10 +289,8 @@ export const ModelSelector = forwardRef( } > diff --git a/src/browser/components/Settings/sections/ModelRow.tsx b/src/browser/components/Settings/sections/ModelRow.tsx index 27d612964..bcf82bbf5 100644 --- a/src/browser/components/Settings/sections/ModelRow.tsx +++ b/src/browser/components/Settings/sections/ModelRow.tsx @@ -107,9 +107,7 @@ export function ModelRow(props: ModelRowProps) { )} aria-label={props.isGatewayEnabled ? "Disable Mux Gateway" : "Enable Mux Gateway"} > - + {props.isGatewayEnabled ? "Using Mux Gateway" : "Use Mux Gateway"} diff --git a/src/browser/components/icons/GatewayIcon.tsx b/src/browser/components/icons/GatewayIcon.tsx index e634dcfdd..170970a6c 100644 --- a/src/browser/components/icons/GatewayIcon.tsx +++ b/src/browser/components/icons/GatewayIcon.tsx @@ -2,13 +2,17 @@ import React from "react"; interface GatewayIconProps extends React.SVGProps { className?: string; + /** When true, shows the active/enabled state with double ring */ + active?: boolean; } /** * Gateway icon - represents routing through Mux Gateway. - * A mystic portal: concentric rings with a sparkle at center. + * Circle with M logo. Active state adds outer ring. */ export function GatewayIcon(props: GatewayIconProps) { + const { active, ...svgProps } = props; + return ( - {/* Outer ring */} - - {/* Inner ring */} - - {/* Center sparkle */} - - + {/* Outer glow ring when active */} + {active && } + {/* Main circle */} + + {/* M letter */} + ); } From cfbf5fd4260e99d426a6d5aa2130a602b8690b17 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:33:29 -0600 Subject: [PATCH 13/16] fix: ensure gateway toggle writes to localStorage synchronously usePersistedState batches localStorage writes in microtask, which can cause race conditions when toggling gateway and immediately sending a message. Now we also call updatePersistedState directly to ensure toGatewayModel() sees the change immediately. --- src/browser/hooks/useGatewayModels.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/browser/hooks/useGatewayModels.ts b/src/browser/hooks/useGatewayModels.ts index 39d36c2ae..172379de6 100644 --- a/src/browser/hooks/useGatewayModels.ts +++ b/src/browser/hooks/useGatewayModels.ts @@ -166,6 +166,8 @@ export function useGateway(): GatewayState { const toggleEnabled = useCallback(() => { setIsEnabled((prev) => !prev); + // Also update localStorage synchronously + updatePersistedState(GATEWAY_ENABLED_KEY, (prev) => !prev); }, [setIsEnabled]); const modelUsesGateway = useCallback( @@ -175,9 +177,15 @@ export function useGateway(): GatewayState { const toggleModelGateway = useCallback( (modelId: string) => { + // Update React state for UI setEnabledModels((prev) => prev.includes(modelId) ? prev.filter((m) => m !== modelId) : [...prev, modelId] ); + // Also update localStorage synchronously so toGatewayModel() sees it immediately + // (usePersistedState batches writes in microtask, which can cause race conditions) + updatePersistedState(GATEWAY_MODELS_KEY, (prev) => + prev.includes(modelId) ? prev.filter((m) => m !== modelId) : [...prev, modelId] + ); }, [setEnabledModels] ); From 9bd29034d09cf7ef360acedd5e652cb9b395b2e7 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:35:49 -0600 Subject: [PATCH 14/16] fix: make useSendMessageOptions reactive to gateway state The hook now subscribes to useGateway() so it re-renders when the user toggles gateway on/off for a model. This fixes the bug where disabling gateway in the model selector didn't take effect immediately when sending a message. --- src/browser/hooks/useSendMessageOptions.ts | 34 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/browser/hooks/useSendMessageOptions.ts b/src/browser/hooks/useSendMessageOptions.ts index 63c12f725..3acce8662 100644 --- a/src/browser/hooks/useSendMessageOptions.ts +++ b/src/browser/hooks/useSendMessageOptions.ts @@ -2,7 +2,7 @@ import { useThinkingLevel } from "./useThinkingLevel"; import { useMode } from "@/browser/contexts/ModeContext"; import { usePersistedState } from "./usePersistedState"; import { getDefaultModel } from "./useModelLRU"; -import { toGatewayModel, migrateGatewayModel } from "./useGatewayModels"; +import { migrateGatewayModel, useGateway, isProviderSupported } from "./useGatewayModels"; import { modeToToolPolicy, PLAN_MODE_INSTRUCTION } from "@/common/utils/ui/modeUtils"; import { getModelKey } from "@/common/constants/storage"; import type { SendMessageOptions } from "@/common/orpc/types"; @@ -12,6 +12,25 @@ import type { MuxProviderOptions } from "@/common/types/providerOptions"; import { getSendOptionsFromStorage } from "@/browser/utils/messages/sendOptions"; import { enforceThinkingPolicy } from "@/browser/utils/thinking/policy"; import { useProviderOptions } from "./useProviderOptions"; +import type { GatewayState } from "./useGatewayModels"; + +/** + * Transform model to gateway format using reactive gateway state. + * This ensures the component re-renders when gateway toggles change. + */ +function applyGatewayTransform(modelId: string, gateway: GatewayState): string { + if (!gateway.isActive || !isProviderSupported(modelId) || !gateway.modelUsesGateway(modelId)) { + return modelId; + } + + // Transform provider:model to mux-gateway:provider/model + const colonIndex = modelId.indexOf(":"); + if (colonIndex === -1) return modelId; + + const provider = modelId.slice(0, colonIndex); + const model = modelId.slice(colonIndex + 1); + return `mux-gateway:${provider}/${model}`; +} /** * Construct SendMessageOptions from raw values @@ -22,7 +41,8 @@ function constructSendMessageOptions( thinkingLevel: ThinkingLevel, preferredModel: string | null | undefined, providerOptions: MuxProviderOptions, - fallbackModel: string + fallbackModel: string, + gateway: GatewayState ): SendMessageOptions { const additionalSystemInstructions = mode === "plan" ? PLAN_MODE_INSTRUCTION : undefined; @@ -33,8 +53,8 @@ function constructSendMessageOptions( // Migrate any legacy mux-gateway:provider/model format to canonical form const baseModel = migrateGatewayModel(rawModel); - // Transform to gateway format if gateway is enabled for this model - const model = toGatewayModel(baseModel); + // Transform to gateway format if gateway is enabled for this model (reactive) + const model = applyGatewayTransform(baseModel, gateway); // Enforce thinking policy at the UI boundary as well (e.g., gpt-5-pro → high only) const uiThinking = enforceThinkingPolicy(model, thinkingLevel); @@ -70,12 +90,16 @@ export function useSendMessageOptions(workspaceId: string): SendMessageOptions { { listener: true } // Listen for changes from ModelSelector and other sources ); + // Subscribe to gateway state so we re-render when user toggles gateway + const gateway = useGateway(); + return constructSendMessageOptions( mode, thinkingLevel, preferredModel, providerOptions, - defaultModel + defaultModel, + gateway ); } From 4b0f98487949cb71fe00995f036ca1fac67f27a9 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:43:43 -0600 Subject: [PATCH 15/16] fix: enforce thinking policy on base model, not gateway-transformed The thinking policy regex patterns expect canonical model format (e.g., 'gpt-5-pro'), not gateway format ('openai/gpt-5-pro'). Run enforceThinkingPolicy before gateway transform to ensure correct policy detection. --- src/browser/hooks/useSendMessageOptions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/hooks/useSendMessageOptions.ts b/src/browser/hooks/useSendMessageOptions.ts index 3acce8662..3a7169d61 100644 --- a/src/browser/hooks/useSendMessageOptions.ts +++ b/src/browser/hooks/useSendMessageOptions.ts @@ -53,12 +53,12 @@ function constructSendMessageOptions( // Migrate any legacy mux-gateway:provider/model format to canonical form const baseModel = migrateGatewayModel(rawModel); + // Enforce thinking policy BEFORE gateway transform (policy checks canonical model name) + const uiThinking = enforceThinkingPolicy(baseModel, thinkingLevel); + // Transform to gateway format if gateway is enabled for this model (reactive) const model = applyGatewayTransform(baseModel, gateway); - // Enforce thinking policy at the UI boundary as well (e.g., gpt-5-pro → high only) - const uiThinking = enforceThinkingPolicy(model, thinkingLevel); - return { thinkingLevel: uiThinking, model, From db56e96822810064574a63ef9c3aed4e0a76a4f1 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 11:48:16 -0600 Subject: [PATCH 16/16] fix: ThinkingSlider uses canonical baseModel for policy checks useSendMessageOptions now returns both: - model: gateway-transformed for API calls - baseModel: canonical format for UI/policy (e.g., codex-max reasoning levels) This ensures ThinkingSlider shows correct options (xhigh for codex-max) even when model is routed through gateway. --- src/browser/components/ChatInput/index.tsx | 7 +++++-- src/browser/hooks/useSendMessageOptions.ts | 23 ++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx index bbc247022..5b73b13aa 100644 --- a/src/browser/components/ChatInput/index.tsx +++ b/src/browser/components/ChatInput/index.tsx @@ -212,8 +212,11 @@ export const ChatInput: React.FC = (props) => { const sendMessageOptions = useSendMessageOptions( variant === "workspace" ? props.workspaceId : getProjectScopeId(props.projectPath) ); - // Extract model for convenience (don't create separate state - use hook as single source of truth) + // Extract models for convenience (don't create separate state - use hook as single source of truth) + // - preferredModel: gateway-transformed model for API calls + // - baseModel: canonical format for UI display and policy checks (e.g., ThinkingSlider) const preferredModel = sendMessageOptions.model; + const baseModel = sendMessageOptions.baseModel; const deferredModel = useDeferredValue(preferredModel); const deferredInput = useDeferredValue(input); const tokenCountPromise = useMemo(() => { @@ -1349,7 +1352,7 @@ export const ChatInput: React.FC = (props) => { className="flex items-center [&_.thinking-slider]:[@container(max-width:550px)]:hidden" data-component="ThinkingSliderGroup" > - +
diff --git a/src/browser/hooks/useSendMessageOptions.ts b/src/browser/hooks/useSendMessageOptions.ts index 3a7169d61..2c8da3799 100644 --- a/src/browser/hooks/useSendMessageOptions.ts +++ b/src/browser/hooks/useSendMessageOptions.ts @@ -69,6 +69,15 @@ function constructSendMessageOptions( }; } +/** + * Extended send options that includes both the gateway-transformed model + * and the base model (for UI components that need canonical model names). + */ +export interface SendMessageOptionsWithBase extends SendMessageOptions { + /** Base model in canonical format (e.g., "openai:gpt-5.1-codex-max") for UI/policy checks */ + baseModel: string; +} + /** * Build SendMessageOptions from current user preferences * This ensures all message sends (new, retry, resume) use consistent options @@ -78,8 +87,11 @@ function constructSendMessageOptions( * * Uses usePersistedState which has listener mode, so changes to preferences * propagate automatically to all components using this hook. + * + * Returns both `model` (possibly gateway-transformed for API calls) and + * `baseModel` (canonical format for UI display and policy checks). */ -export function useSendMessageOptions(workspaceId: string): SendMessageOptions { +export function useSendMessageOptions(workspaceId: string): SendMessageOptionsWithBase { const [thinkingLevel] = useThinkingLevel(); const [mode] = useMode(); const { options: providerOptions } = useProviderOptions(); @@ -93,7 +105,12 @@ export function useSendMessageOptions(workspaceId: string): SendMessageOptions { // Subscribe to gateway state so we re-render when user toggles gateway const gateway = useGateway(); - return constructSendMessageOptions( + // Compute base model (canonical format) for UI components + const rawModel = + typeof preferredModel === "string" && preferredModel ? preferredModel : defaultModel; + const baseModel = migrateGatewayModel(rawModel); + + const options = constructSendMessageOptions( mode, thinkingLevel, preferredModel, @@ -101,6 +118,8 @@ export function useSendMessageOptions(workspaceId: string): SendMessageOptions { defaultModel, gateway ); + + return { ...options, baseModel }; } /**