Skip to content

Commit d2e53f6

Browse files
authored
🤖 fix(openai): omit service_tier when not configured (#3212)
## Summary Stop writing `serviceTier: "auto"` into provider options when the user hasn't configured one, and surface that "unset" state explicitly in the Settings UI and CLI. The provider field is now omitted entirely when no value is selected, letting OpenAI apply its own default routing. ## Background Previously the OpenAI service tier silently defaulted to `"auto"` everywhere: in `buildProviderOptions`, in the CLI's `--service-tier` flag, and in the Settings dropdown. That meant requests always sent `service_tier: auto` even when the user never picked a tier, and the UI couldn't represent the actual "no preference" state at all (the dropdown showed `auto` whether the config was missing or explicitly set to `auto`). ## Implementation - `buildProviderOptions` now reads `muxProviderOptions?.openai?.serviceTier` directly and only spreads `serviceTier` into the OpenAI options when it is set. - `src/cli/run.ts` drops the `"auto"` default from `--service-tier`; if the flag is omitted, the entire `openai` block is left out of `providerOptions`. - `ProvidersSection` adds a sentinel `"unset"` Select item labeled `Not configured (omit service_tier)`. Picking it sends `value: ""` through `setProviderConfig`, which the backend treats as a delete, removing the `serviceTier` key from `providers.jsonc`. A short-lived `openaiServiceTierSelectOverride` keeps the trigger label correct between optimistic update and refresh. - `providerModelFactory` likewise no longer coerces a missing `serviceTier` into `"auto"`. ## Validation - Sandboxed dev-server walk-through (clean install -> `flex` -> back to unset -> explicit `auto`); each transition verified against `providers.jsonc` on disk and the dropdown trigger label. - `make static-check` (eslint, tsgo x2, prettier, code-to-docs links, shellcheck, hadolint) clean. - `bun test src/cli/run.test.ts src/common/utils/ai/providerOptions.test.ts src/node/services/providerService.test.ts` (176 pass). ## Risks Low. Behavior change is limited to OpenAI requests: when no tier was previously configured, requests will now omit `service_tier` instead of sending `"auto"`. OpenAI treats both as default routing, so live traffic should be unaffected. Existing configs with an explicit value continue to send that value verbatim. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh` • Cost: `$47.05`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh costs=47.05 -->
1 parent f79108e commit d2e53f6

7 files changed

Lines changed: 110 additions & 14 deletions

File tree

src/browser/features/Settings/Sections/ProvidersSection.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ type MuxGatewayLoginStatus = "idle" | "starting" | "waiting" | "success" | "erro
8383
type CodexOauthFlowStatus = "idle" | "starting" | "waiting" | "error";
8484
type CopilotLoginStatus = "idle" | "starting" | "waiting" | "success" | "error";
8585

86+
const OPENAI_SERVICE_TIER_UNSET = "unset";
87+
88+
type OpenAIServiceTier = "auto" | "default" | "flex" | "priority";
89+
type OpenAIServiceTierSelectValue = typeof OPENAI_SERVICE_TIER_UNSET | OpenAIServiceTier;
90+
91+
function isOpenAIServiceTier(value: string): value is OpenAIServiceTier {
92+
return value === "auto" || value === "default" || value === "flex" || value === "priority";
93+
}
94+
8695
interface CodexOauthDeviceFlow {
8796
flowId: string;
8897
userCode: string;
@@ -382,6 +391,9 @@ export function ProvidersSection() {
382391
refresh: refreshMuxGatewayAccountStatus,
383392
} = useMuxGatewayAccountStatus();
384393

394+
const [openaiServiceTierSelectOverride, setOpenaiServiceTierSelectOverride] =
395+
useState<OpenAIServiceTierSelectValue | null>(null);
396+
385397
const routing = useRouting();
386398

387399
const providerGroups = useMemo(() => {
@@ -2445,18 +2457,32 @@ export function ProvidersSection() {
24452457
</TooltipProvider>
24462458
</div>
24472459
<Select
2448-
value={config?.openai?.serviceTier ?? "auto"}
2460+
value={
2461+
openaiServiceTierSelectOverride ??
2462+
config?.openai?.serviceTier ??
2463+
OPENAI_SERVICE_TIER_UNSET
2464+
}
24492465
onValueChange={(next) => {
24502466
if (!api) return;
2451-
if (
2452-
next !== "auto" &&
2453-
next !== "default" &&
2454-
next !== "flex" &&
2455-
next !== "priority"
2456-
) {
2467+
2468+
if (next === OPENAI_SERVICE_TIER_UNSET) {
2469+
setOpenaiServiceTierSelectOverride(OPENAI_SERVICE_TIER_UNSET);
2470+
void api.providers
2471+
.setProviderConfig({
2472+
provider: "openai",
2473+
keyPath: ["serviceTier"],
2474+
value: "",
2475+
})
2476+
.then(() => refresh())
2477+
.finally(() => setOpenaiServiceTierSelectOverride(null));
2478+
return;
2479+
}
2480+
2481+
if (!isOpenAIServiceTier(next)) {
24572482
return;
24582483
}
24592484

2485+
setOpenaiServiceTierSelectOverride(null);
24602486
updateOptimistically("openai", { serviceTier: next });
24612487
void api.providers.setProviderConfig({
24622488
provider: "openai",
@@ -2465,10 +2491,13 @@ export function ProvidersSection() {
24652491
});
24662492
}}
24672493
>
2468-
<SelectTrigger className="w-40">
2494+
<SelectTrigger className="w-64">
24692495
<SelectValue />
24702496
</SelectTrigger>
24712497
<SelectContent>
2498+
<SelectItem value={OPENAI_SERVICE_TIER_UNSET}>
2499+
Not configured (omit service_tier)
2500+
</SelectItem>
24722501
<SelectItem value="auto">auto</SelectItem>
24732502
<SelectItem value="default">default</SelectItem>
24742503
<SelectItem value="flex">flex</SelectItem>

src/cli/run.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ describe("mux CLI", () => {
167167
expect(result.stdout).toContain("anthropic:claude-opus-4-7");
168168
});
169169

170+
test("--service-tier has no default auto", async () => {
171+
const result = await runCli(["run", "--help"]);
172+
expect(result.exitCode).toBe(0);
173+
expect(result.stdout).toContain("--service-tier");
174+
expect(result.stdout).not.toContain("[default: auto]");
175+
expect(result.stdout).not.toContain('[default: "auto"]');
176+
});
177+
170178
test("no message shows error", async () => {
171179
const result = await runRunDirect([]);
172180
expect(result.exitCode).toBe(1);

src/cli/run.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ program
259259
.option("--no-mcp-config", "ignore global + repo MCP config files (use only --mcp servers)")
260260
.option("-e, --experiment <id>", "enable experiment (can be repeated)", collectExperiments, [])
261261
.option("-b, --budget <usd>", "stop when session cost exceeds budget (USD)", parseFloat)
262-
.option("--service-tier <tier>", "OpenAI service tier: auto, default, flex, priority", "auto")
262+
.option("--service-tier <tier>", "OpenAI service tier: auto, default, flex, priority")
263263
.option("--use-1m", "enable 1M context window for supported Anthropic models")
264264
.option(
265265
"--keep-background-processes",
@@ -298,7 +298,7 @@ interface CLIOptions {
298298
mcpConfig: boolean;
299299
experiment: string[];
300300
budget?: number;
301-
serviceTier: "auto" | "default" | "flex" | "priority";
301+
serviceTier?: "auto" | "default" | "flex" | "priority";
302302
use1m?: boolean;
303303
keepBackgroundProcesses?: boolean;
304304
}
@@ -612,7 +612,7 @@ async function main(): Promise<number> {
612612
experiments,
613613
providerOptions: {
614614
...(opts.use1m && { anthropic: { use1MContext: true } }),
615-
openai: { serviceTier: opts.serviceTier },
615+
...(opts.serviceTier != null && { openai: { serviceTier: opts.serviceTier } }),
616616
},
617617
// Disable UI-only tools that have no effect in CLI mode:
618618
// - status_set: backend no-op, status indicator only visible in desktop UI

src/common/utils/ai/providerOptions.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,48 @@ describe("buildProviderOptions - OpenAI", () => {
539539
});
540540
});
541541

542+
describe("serviceTier option", () => {
543+
test("should not include serviceTier key when muxProviderOptions is omitted", () => {
544+
const result = buildProviderOptions("openai:gpt-5", "medium");
545+
const openai = getOpenAIOptions(result);
546+
547+
expect(openai).toBeDefined();
548+
expect("serviceTier" in openai!).toBe(false);
549+
});
550+
551+
test("should not include serviceTier key when muxProviderOptions.openai.serviceTier is undefined", () => {
552+
const result = buildProviderOptions("openai:gpt-5", "medium", undefined, undefined, {
553+
openai: { serviceTier: undefined },
554+
});
555+
const openai = getOpenAIOptions(result);
556+
557+
expect(openai).toBeDefined();
558+
expect("serviceTier" in openai!).toBe(false);
559+
});
560+
561+
test("should include serviceTier: auto when explicitly set", () => {
562+
const result = buildProviderOptions("openai:gpt-5", "medium", undefined, undefined, {
563+
openai: { serviceTier: "auto" },
564+
});
565+
const openai = getOpenAIOptions(result);
566+
567+
expect(openai).toBeDefined();
568+
expect("serviceTier" in openai!).toBe(true);
569+
expect(openai!.serviceTier).toBe("auto");
570+
});
571+
572+
test("should include explicit non-auto serviceTier", () => {
573+
const result = buildProviderOptions("openai:gpt-5", "medium", undefined, undefined, {
574+
openai: { serviceTier: "flex" },
575+
});
576+
const openai = getOpenAIOptions(result);
577+
578+
expect(openai).toBeDefined();
579+
expect("serviceTier" in openai!).toBe(true);
580+
expect(openai!.serviceTier).toBe("flex");
581+
});
582+
});
583+
542584
describe("promptCacheKey derivation", () => {
543585
test("should prefer promptCacheScope over workspaceId for promptCacheKey", () => {
544586
const result = buildProviderOptions(

src/common/utils/ai/providerOptions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ export function buildProviderOptions(
358358
const cacheScope = promptCacheScope ?? workspaceId;
359359
const promptCacheKey = cacheScope ? `mux-v1-${cacheScope}` : undefined;
360360

361-
const serviceTier = muxProviderOptions?.openai?.serviceTier ?? "auto";
361+
const serviceTier = muxProviderOptions?.openai?.serviceTier;
362362
const wireFormat = muxProviderOptions?.openai?.wireFormat ?? "responses";
363363
const store = muxProviderOptions?.openai?.store;
364364
const isResponses = wireFormat === "responses";
@@ -378,7 +378,7 @@ export function buildProviderOptions(
378378
const options = {
379379
openai: {
380380
parallelToolCalls: true, // Always enable concurrent tool execution
381-
serviceTier,
381+
...(serviceTier != null && { serviceTier }),
382382
...(store != null && { store }), // ZDR: pass store flag through to OpenAI SDK
383383
...(isResponses && {
384384
// Default to disabled; allow auto truncation for compaction to avoid context errors

src/node/services/providerModelFactory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1276,7 +1276,7 @@ export class ProviderModelFactory {
12761276
const configWireFormat = providerConfig.wireFormat as string | undefined;
12771277
if (configServiceTier || configWireFormat) {
12781278
muxProviderOptions ??= {};
1279-
if (configServiceTier) {
1279+
if (configServiceTier && muxProviderOptions.openai?.serviceTier == null) {
12801280
muxProviderOptions.openai = {
12811281
...muxProviderOptions.openai,
12821282
serviceTier: configServiceTier as "auto" | "default" | "flex" | "priority",

src/node/services/providerService.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,23 @@ describe("ProviderService.setConfig", () => {
12261226
});
12271227
});
12281228

1229+
it("removes OpenAI serviceTier when set to an empty string", async () => {
1230+
await withTempConfigAsync(async (config, service) => {
1231+
config.saveProvidersConfig({
1232+
openai: {
1233+
apiKey: "sk-test",
1234+
serviceTier: "auto",
1235+
},
1236+
});
1237+
1238+
const result = await service.setConfig("openai", ["serviceTier"], "");
1239+
1240+
expect(result.success).toBe(true);
1241+
expect(config.loadProvidersConfig()?.openai?.serviceTier).toBeUndefined();
1242+
expect(Object.hasOwn(config.loadProvidersConfig()?.openai ?? {}, "serviceTier")).toBe(false);
1243+
});
1244+
});
1245+
12291246
it("stores enabled=false without deleting existing credentials", async () => {
12301247
await withTempConfigAsync(async (config, service) => {
12311248
config.saveProvidersConfig({

0 commit comments

Comments
 (0)