Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ jobs:
fi
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
BACKEND: ${{ needs.changes.outputs.backend }}
BROWSER: ${{ needs.changes.outputs.browser }}
Expand Down
134 changes: 28 additions & 106 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions docs/config/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ Mux ships with curated models kept up to date with the frontier. Use any custom
| Codex Max 5.1 | openai:gpt-5.1-codex-max | `codex-max` | |
| Gemini 3.1 Pro Preview | google:gemini-3.1-pro-preview | `gemini`, `gemini-pro` | |
| Gemini 3.6 Flash | google:gemini-3.6-flash | `gemini-flash` | |
| Grok 4 1 Fast | xai:grok-4-1-fast | `grok`, `grok-4`, `grok-4.1`, `grok-4-1` | |
| Grok Code Fast 1 | xai:grok-code-fast-1 | `grok-code` | |
| Grok 4.5 | xai:grok-4.5 | `grok`, `grok-4.5` | |
| DeepSeek V4 Pro | deepseek:deepseek-v4-pro | `deepseek`, `deepseek-pro`, `deepseek-v4`, `deepseek-v4-pro` | |
| DeepSeek V4 Flash | deepseek:deepseek-v4-flash | `deepseek-flash`, `deepseek-v4-flash` | |
| Kimi K3 | moonshotai:kimi-k3 | `kimi`, `k3`, `kimi-k3` | |
Expand Down
9 changes: 5 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@
stdenv.cc.cc.lib # Provides libstdc++ for native modules like sharp
];

# Fetch dependencies in a separate fixed-output derivation
# Use only package.json and bun.lock to ensure consistent hashing
# regardless of how the flake is evaluated (local vs remote)
# Fetch dependencies in a separate fixed-output derivation.
# Include Bun patch files alongside package.json and bun.lock so patched
# dependencies install identically in local and remote Nix evaluations.
offlineCache = pkgs.stdenvNoCC.mkDerivation {
name = "mux-deps-${version}";

src = pkgs.runCommand "mux-lock-files" { } ''
mkdir -p $out
cp ${./package.json} $out/package.json
cp -r ${./patches} $out/patches
cp ${./bun.lock} $out/bun.lock
'';

Expand Down Expand Up @@ -83,7 +84,7 @@

outputHashMode = "recursive";
# Marker used by scripts/update_flake_hash.sh to update this hash in place.
outputHash = "sha256-hK0zwN0pShMv8NlfMXud+9Jx2J32hz5+qtLMqzunZF8="; # mux-offline-cache-hash
outputHash = "sha256-ufRtgBViq/5RAGzL/f79pIYK/+3BCQNNAD1nCz/edRg="; # mux-offline-cache-hash
};

configurePhase = ''
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@ai-sdk/moonshotai": "^3.0.15",
"@ai-sdk/openai": "^4.0.11",
"@ai-sdk/openai-compatible": "^3.0.7",
"@ai-sdk/xai": "^4.0.10",
"@ai-sdk/xai": "^4.0.28",
"@aws-sdk/credential-providers": "^3.940.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
Expand Down Expand Up @@ -252,6 +252,7 @@
"dist/**/*.json",
"dist/**/*.png",
"dist/assets/**/*",
"patches/**/*",
"scripts/postinstall.sh",
"npm-shrinkwrap.json",
"README.md",
Expand Down Expand Up @@ -331,5 +332,8 @@
"sharp",
"ssh2",
"unrs-resolver"
]
],
"patchedDependencies": {
"@ai-sdk/xai@4.0.28": "patches/@ai-sdk%2Fxai@4.0.28.patch"
}
}
30 changes: 30 additions & 0 deletions patches/@ai-sdk%2Fxai@4.0.28.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
diff --git a/dist/index.js b/dist/index.js
index ea833a84c3323dcc72534e076d68473dfdb77128..41568478bea4246ff9acd7943bbd4d4ee6bd28e3 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -2096,8 +2096,8 @@ import { z as z12 } from "zod/v4";
var xSearchArgsSchema = lazySchema4(
() => zodSchema4(
z12.object({
- allowedXHandles: z12.array(z12.string()).max(10).optional(),
- excludedXHandles: z12.array(z12.string()).max(10).optional(),
+ allowedXHandles: z12.array(z12.string()).max(20).optional(),
+ excludedXHandles: z12.array(z12.string()).max(20).optional(),
fromDate: z12.string().optional(),
toDate: z12.string().optional(),
enableImageUnderstanding: z12.boolean().optional(),
diff --git a/src/tool/x-search.ts b/src/tool/x-search.ts
index 5566aa9c391f67cab60fe50ff2a2b55ca71b0b73..6d0dd586f07c5ea9d2bd2dcbed4be211be2e3c8a 100644
--- a/src/tool/x-search.ts
+++ b/src/tool/x-search.ts
@@ -8,8 +8,8 @@ import { z } from 'zod/v4';
export const xSearchArgsSchema = lazySchema(() =>
zodSchema(
z.object({
- allowedXHandles: z.array(z.string()).max(10).optional(),
- excludedXHandles: z.array(z.string()).max(10).optional(),
+ allowedXHandles: z.array(z.string()).max(20).optional(),
+ excludedXHandles: z.array(z.string()).max(20).optional(),
fromDate: z.string().optional(),
toDate: z.string().optional(),
enableImageUnderstanding: z.boolean().optional(),
1 change: 1 addition & 0 deletions scripts/check-bench-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ trap 'rm -rf "$CHECK_DIR"' EXIT

# Copy built dist and package.json — but NO lockfile, shrinkwrap, or node_modules.
cp -r "$REPO_ROOT/dist" "$CHECK_DIR/dist"
cp -r "$REPO_ROOT/patches" "$CHECK_DIR/patches"
cp "$REPO_ROOT/package.json" "$CHECK_DIR/package.json"

# Strip devDependencies so bun only resolves production deps (matching published package).
Expand Down
38 changes: 24 additions & 14 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "./hooks/usePersistedState";
import { useResizableSidebar } from "./hooks/useResizableSidebar";
import { matchesKeybind, KEYBINDS } from "./utils/ui/keybinds";
import { applyFastModeServiceTierChange } from "./utils/fastModeServiceTier";
import { applyFastModeServiceTierChange, getFastModeProvider } from "./utils/fastModeServiceTier";
import { handleLayoutSlotHotkeys } from "./utils/ui/layoutSlotHotkeys";
import { buildSortedWorkspacesByProject } from "./utils/ui/workspaceFiltering";
import {
Expand Down Expand Up @@ -76,7 +76,6 @@ import {
LEFT_SIDEBAR_WIDTH_KEY,
} from "@/common/constants/storage";
import { normalizeToCanonical } from "@/common/utils/ai/models";
import { openaiDirectProviderOptionsAvailable } from "@/common/utils/ai/openaiProviderOptionsAvailability";
import { getDefaultModel } from "@/browser/hooks/useModelsFromSettings";
import type { BranchListResult } from "@/common/orpc/types";
import { useTelemetry } from "./hooks/useTelemetry";
Expand Down Expand Up @@ -680,8 +679,19 @@ function AppInner() {
[api, getModelForWorkspace, getReasoningModeForWorkspace, getThinkingLevelForWorkspace]
);

const getFastModeActive = useCallback(() => {
const scopeId = selectedWorkspace?.workspaceId ?? creationScopeId;
if (!scopeId || providersConfig == null) return false;

const model = getModelForWorkspace(scopeId);
const provider = getFastModeProvider(model, {
providersConfig,
resolvedRouteProvider: getRouteForModel(normalizeToCanonical(model)),
});
return provider != null && providersConfig[provider]?.serviceTier === "priority";
}, [creationScopeId, getModelForWorkspace, getRouteForModel, providersConfig, selectedWorkspace]);

const fastModeToggleInFlightRef = useRef(false);
const fastModeActive = providersConfig?.openai?.serviceTier === "priority";
const toggleFastMode = useCallback(async () => {
const scopeId = selectedWorkspace?.workspaceId ?? creationScopeId;
if (!api || !scopeId || providersConfig == null || fastModeToggleInFlightRef.current) return;
Expand All @@ -691,25 +701,25 @@ function AppInner() {
// Serialize requests so a quick double press cannot compute two writes from stale config.
fastModeToggleInFlightRef.current = true;
const model = getModelForWorkspace(scopeId);
const route = getRouteForModel(normalizeToCanonical(model));
if (
!openaiDirectProviderOptionsAvailable(model, {
providersConfig,
resolvedRouteProvider: route,
})
) {
const provider = getFastModeProvider(model, {
providersConfig,
resolvedRouteProvider: getRouteForModel(normalizeToCanonical(model)),
});
if (provider == null) {
fastModeToggleInFlightRef.current = false;
return;
}

try {
const providerConfig = providersConfig[provider];
const change = await applyFastModeServiceTierChange(
api.providers,
providersConfig.openai?.serviceTier,
providersConfig.openai?.fastModePreviousServiceTier
provider,
providerConfig?.serviceTier,
providerConfig?.fastModePreviousServiceTier
);
if (change) {
updateOptimistically("openai", {
updateOptimistically(provider, {
serviceTier: change.serviceTier,
fastModePreviousServiceTier: change.previousServiceTier,
});
Expand Down Expand Up @@ -972,7 +982,7 @@ function AppInner() {
onSetThinkingLevel: setThinkingLevelFromPalette,
getReasoningMode: getReasoningModeForWorkspace,
onToggleReasoningMode: toggleReasoningModeFromPalette,
getFastMode: () => fastModeActive,
getFastMode: getFastModeActive,
onToggleFastMode: toggleFastMode,
getEffectiveComposerModel: getModelForWorkspace,
providersConfig,
Expand Down
1 change: 0 additions & 1 deletion src/browser/components/ModelSelector/modelFilter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ describe("modelMatchesQuery", () => {
test("matches documented aliases that are not model string substrings", () => {
expect(modelMatchesQuery("google:gemini-3.6-flash", "gemini-flash")).toBe(true);
expect(modelMatchesQuery("deepseek:deepseek-v4-pro", "deepseek-pro")).toBe(true);
expect(modelMatchesQuery("xai:grok-4-1-fast", "grok-4.1")).toBe(true);
});

test("matches aliases for gateway-prefixed model strings", () => {
Expand Down
33 changes: 20 additions & 13 deletions src/browser/components/ThinkingSelector/ThinkingSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { useReasoningMode } from "@/browser/hooks/useReasoningMode";
import { useRouting } from "@/browser/hooks/useRouting";
import { useThinkingLevel } from "@/browser/hooks/useThinkingLevel";
import { stopKeyboardPropagation } from "@/browser/utils/events";
import { applyFastModeServiceTierChange } from "@/browser/utils/fastModeServiceTier";
import {
applyFastModeServiceTierChange,
getFastModeProvider,
} from "@/browser/utils/fastModeServiceTier";
import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
import { cn } from "@/common/lib/utils";
import { getThinkingDisplayLabel, type ThinkingLevel } from "@/common/types/thinking";
import { openaiDirectProviderOptionsAvailable } from "@/common/utils/ai/openaiProviderOptionsAvailability";
import { openaiProModeAvailable } from "@/common/utils/ai/proMode";
import { normalizeToCanonical } from "@/common/utils/ai/models";
import { enforceThinkingPolicy, getAvailableThinkingLevels } from "@/common/utils/thinking/policy";
Expand Down Expand Up @@ -60,20 +62,23 @@ export const ThinkingSelector: React.FC<ThinkingSelectorProps> = (props) => {
);
const displayLabel = getThinkingDisplayLabel(effectiveThinkingLevel, props.modelString);
const resolvedRoute = routing.resolveRoute(normalizeToCanonical(props.modelString)).route;
const directOpenAIOptionsAvailable = openaiDirectProviderOptionsAvailable(props.modelString, {
providersConfig,
resolvedRouteProvider: resolvedRoute,
});
const proModeAvailable =
props.allowProMode !== false &&
openaiProModeAvailable(props.modelString, {
providersConfig,
resolvedRouteProvider: resolvedRoute,
});
const fastModeAvailable =
props.allowFastMode !== false && providersConfig != null && directOpenAIOptionsAvailable;
const fastModeProvider =
props.allowFastMode !== false && providersConfig != null
? getFastModeProvider(props.modelString, {
providersConfig,
resolvedRouteProvider: resolvedRoute,
})
: null;
const fastModeAvailable = fastModeProvider != null;
const proModeActive = proModeAvailable && reasoningMode === "pro";
const fastModeActive = fastModeAvailable && providersConfig?.openai?.serviceTier === "priority";
const fastModeActive =
fastModeProvider != null && providersConfig?.[fastModeProvider]?.serviceTier === "priority";
const hasMenu = allowed.length > 1 || proModeAvailable || fastModeAvailable;

// The menu is rendered inline for happy-dom coverage, so a document listener is the
Expand All @@ -92,17 +97,19 @@ export const ThinkingSelector: React.FC<ThinkingSelectorProps> = (props) => {
}, [isOpen]);

const handleFastModeToggle = async () => {
if (!api || fastModeSaving || providersConfig == null) return;
if (!api || fastModeSaving || providersConfig == null || fastModeProvider == null) return;

setFastModeSaving(true);
try {
const providerConfig = providersConfig[fastModeProvider];
const change = await applyFastModeServiceTierChange(
api.providers,
providersConfig.openai?.serviceTier,
providersConfig.openai?.fastModePreviousServiceTier
fastModeProvider,
providerConfig?.serviceTier,
providerConfig?.fastModePreviousServiceTier
);
if (change) {
updateOptimistically("openai", {
updateOptimistically(fastModeProvider, {
serviceTier: change.serviceTier,
fastModePreviousServiceTier: change.previousServiceTier,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,33 @@ export const ProvidersEnvSourced: Story = {
},
};

export const XAIProcessingMode: Story = {
render: () => (
<SettingsSectionStory
setup={() =>
setupSettingsStory({
providersConfig: {
xai: {
apiKeySet: true,
isEnabled: true,
isConfigured: true,
serviceTier: "priority",
},
},
})
}
>
<ProvidersSection />
</SettingsSectionStory>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const xaiButton = await canvas.findByRole("button", { name: /xAI/i });
await userEvent.click(xaiButton);
await canvas.findByText("fast (priority)");
},
};

export const ProvidersExpanded: Story = {
render: () => (
<SettingsSectionStory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ function createProvidersConfig(): ProvidersConfigMap {
isEnabled: true,
isConfigured: true,
},
xai: {
apiKeySet: true,
isEnabled: true,
isConfigured: true,
},
[CUSTOM_PROVIDER_ID]: {
apiKeySet: true,
isEnabled: true,
Expand Down
Loading
Loading