Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const XAIProcessingMode: Story = {
isEnabled: true,
isConfigured: true,
serviceTier: "priority",
store: false,
},
},
})
Expand All @@ -127,6 +128,8 @@ export const XAIProcessingMode: Story = {
const xaiButton = await canvas.findByRole("button", { name: /xAI/i });
await userEvent.click(xaiButton);
await canvas.findByText("fast (priority)");
// ZDR response storage control is available alongside processing mode.
await canvas.findByText("Response storage");
},
};

Expand Down
173 changes: 114 additions & 59 deletions src/browser/features/Settings/Sections/ProvidersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2666,68 +2666,123 @@ export function ProvidersSection() {
})()}

{provider === "xai" && (
<div className="border-border-light border-t pt-3">
<div className="mb-1 flex items-center gap-1">
<label className="text-muted block text-xs">Processing mode</label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpIndicator aria-label="xAI processing mode help">
?
</HelpIndicator>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-[260px]">
<div className="font-semibold">xAI processing mode</div>
<div className="mt-1">
<span className="font-semibold">standard</span>: normal
scheduling and token pricing.
</div>
<div>
<span className="font-semibold">fast</span>: priority
scheduling for lower latency at 2Ă— token pricing.
<div className="border-border-light space-y-3 border-t pt-3">
<div>
<div className="mb-1 flex items-center gap-1">
<label className="text-muted block text-xs">Processing mode</label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpIndicator aria-label="xAI processing mode help">
?
</HelpIndicator>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-[260px]">
<div className="font-semibold">xAI processing mode</div>
<div className="mt-1">
<span className="font-semibold">standard</span>: normal
scheduling and token pricing.
</div>
<div>
<span className="font-semibold">fast</span>: priority
scheduling for lower latency at 2Ă— token pricing.
</div>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select
value={config?.xai?.serviceTier ?? "default"}
disabled={xaiServiceTierSaving}
onValueChange={(next) => {
if (!api || xaiServiceTierSaving || !isXAIServiceTier(next)) return;

// Persist before publishing the new tier so the composer cannot
// observe Fast mode until the backend will apply it to requests.
setXAIServiceTierSaving(true);
void api.providers
.setProviderConfig({
provider: "xai",
keyPath: ["serviceTier"],
value: next,
})
.then(
(result) => {
if (result.success) {
updateOptimistically("xai", { serviceTier: next });
return undefined;
}
return refresh();
},
() => refresh()
)
.finally(() => setXAIServiceTierSaving(false));
}}
>
<SelectTrigger className="w-48 max-w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">standard</SelectItem>
<SelectItem value="priority">fast (priority)</SelectItem>
</SelectContent>
</Select>
</div>
<Select
value={config?.xai?.serviceTier ?? "default"}
disabled={xaiServiceTierSaving}
onValueChange={(next) => {
if (!api || xaiServiceTierSaving || !isXAIServiceTier(next)) return;

// Persist before publishing the new tier so the composer cannot
// observe Fast mode until the backend will apply it to requests.
setXAIServiceTierSaving(true);
void api.providers
.setProviderConfig({

<div>
<div className="mb-1 flex items-center gap-1">
<label className="text-muted block text-xs">Response storage</label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpIndicator aria-label="xAI response storage help">
?
</HelpIndicator>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-[260px]">
<div className="font-semibold">xAI response storage</div>
<div className="mt-1">
<span className="font-semibold">enabled</span>: xAI stores
responses for retrieval and context (default).
</div>
<div>
<span className="font-semibold">disabled</span>: responses
are not stored. Required for zero data retention (ZDR) orgs.
Encrypted reasoning is still preserved client-side so
multi-turn quality matches non-ZDR.
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select
value={config?.xai?.store === false ? "disabled" : "enabled"}
onValueChange={(next) => {
if (!api) return;
if (next !== "enabled" && next !== "disabled") return;

const store = next === "disabled" ? false : undefined;
updateOptimistically("xai", { store });
void api.providers.setProviderConfig({
Comment thread
ammar-agent marked this conversation as resolved.
Outdated
provider: "xai",
keyPath: ["serviceTier"],
value: next,
})
.then(
(result) => {
if (result.success) {
updateOptimistically("xai", { serviceTier: next });
return undefined;
}
return refresh();
},
() => refresh()
)
.finally(() => setXAIServiceTierSaving(false));
}}
>
<SelectTrigger className="w-48 max-w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">standard</SelectItem>
<SelectItem value="priority">fast (priority)</SelectItem>
</SelectContent>
</Select>
keyPath: ["store"],
value: next === "disabled" ? false : "",
});
}}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="enabled">enabled</SelectItem>
<SelectItem value="disabled">disabled</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}

Expand Down
2 changes: 2 additions & 0 deletions src/browser/features/Settings/Sections/settingsStoryUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ interface SetupSettingsStoryOptions {
baseUrlSource?: "config" | "env";
baseUrlResolved?: string;
serviceTier?: ServiceTier;
/** OpenAI/xAI Responses storage (false for ZDR). */
store?: boolean;
models?: string[];
}
>;
Expand Down
6 changes: 6 additions & 0 deletions src/common/config/schemas/providersConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ describe("ProvidersConfigSchema", () => {
).toBe(false);
});

it("accepts xAI store flag for ZDR", () => {
expect(ProvidersConfigSchema.safeParse({ xai: { store: false } }).success).toBe(true);
expect(ProvidersConfigSchema.safeParse({ xai: { store: true } }).success).toBe(true);
expect(ProvidersConfigSchema.safeParse({ xai: { store: "false" } }).success).toBe(false);
});

describe("modelParameters", () => {
it("accepts valid per-model and wildcard overrides", () => {
const valid = {
Expand Down
2 changes: 2 additions & 0 deletions src/common/config/schemas/providersConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export const XAIProviderConfigSchema = BaseProviderConfigSchema.extend({
searchParameters: z.record(z.string(), z.unknown()).optional(),
serviceTier: XAIServiceTierSchema.optional(),
fastModePreviousServiceTier: XAIFastModePreviousServiceTierSchema.optional(),
// Required for xAI ZDR orgs on Grok 4.5 Responses (same semantics as OpenAI store).
store: z.boolean().optional(),
});

export const MuxGatewayProviderConfigSchema = BaseProviderConfigSchema.extend({
Expand Down
1 change: 1 addition & 0 deletions src/common/orpc/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export const ProviderConfigInfoSchema = z.object({
serviceTier: ServiceTierSchema.optional(),
fastModePreviousServiceTier: FastModePreviousServiceTierSchema.optional(),
wireFormat: z.enum(["responses", "chatCompletions"]).optional(),
/** OpenAI/xAI Responses storage. Set false for ZDR orgs. */
store: z.boolean().optional(),
webSocketTransportEnabled: z.boolean().optional(),
/** Anthropic-specific fields */
Expand Down
6 changes: 6 additions & 0 deletions src/common/schemas/providerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export const MuxProviderOptionsSchema = z.object({
description:
'xAI processing tier: "priority" requests faster processing at 2Ă— token pricing; "default" uses standard processing',
}),
// Grok 4.5 Responses defaults to store=true; ZDR orgs must set false or
// requests fail. @ai-sdk/xai then auto-includes reasoning.encrypted_content
// so multi-turn tool use can keep reasoning quality without server storage.
store: z.boolean().optional().meta({
description: "Whether xAI stores responses. Set false for zero data retention (ZDR).",
}),
searchParameters: z
.object({
mode: z.enum(["auto", "off", "on"]),
Expand Down
14 changes: 12 additions & 2 deletions src/common/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,13 +657,23 @@ export interface MuxReasoningPart {
/**
* Provider options for SDK compatibility.
* When converting to ModelMessages via the SDK's convertToModelMessages,
* this is passed through. For Anthropic thinking blocks, this should contain
* { anthropic: { signature } } to allow reasoning replay.
* this is passed through so reasoning can be replayed:
* - Anthropic: { anthropic: { signature } }
* - OpenAI/xAI Responses (esp. store=false/ZDR): itemId + reasoningEncryptedContent
* so the next turn can restore encrypted reasoning without server-side storage.
*/
providerOptions?: {
anthropic?: {
signature?: string;
};
openai?: {
itemId?: string;
reasoningEncryptedContent?: string | null;
};
xai?: {
itemId?: string;
reasoningEncryptedContent?: string | null;
};
};
}

Expand Down
22 changes: 22 additions & 0 deletions src/common/utils/ai/providerOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,28 @@ describe("buildProviderOptions - xAI", () => {
},
});
});

test("includes store: false for xAI ZDR without dropping reasoning effort", () => {
expect(
buildProviderOptions("xai:grok-4.5", "medium", undefined, undefined, {
xai: { store: false },
})
).toEqual({
xai: {
reasoningEffort: "medium",
store: false,
},
});
});

test("omits store key when xAI store is unset", () => {
const result = buildProviderOptions("xai:grok-4.5", "medium", undefined, undefined, {
xai: {},
});
const xai = (result as { xai?: Record<string, unknown> }).xai;
expect(xai).toBeDefined();
expect("store" in xai!).toBe(false);
});
});

describe("buildRequestHeaders", () => {
Expand Down
21 changes: 18 additions & 3 deletions src/common/utils/ai/providerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import type { AnthropicProviderOptions } from "@ai-sdk/anthropic";
import type { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google";
import type { OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import type { JSONValue } from "@ai-sdk/provider";
import type { XaiProviderOptions } from "@ai-sdk/xai";
import type {
XaiProviderOptions,
// Chat options alias does not include store; Responses options do (Grok 4.5 / ZDR).
XaiResponsesProviderOptions,
} from "@ai-sdk/xai";
import type { ProviderName } from "@/common/constants/providers";
import type { ProvidersConfigMap } from "@/common/orpc/types";
import type { MuxProviderOptions } from "@/common/types/providerOptions";
Expand Down Expand Up @@ -73,6 +77,12 @@ interface MoonshotAIProviderOptions {
reasoningEffort?: "max";
}

/**
* xAI providerOptions payload. Chat models use XaiProviderOptions; Grok 4.5
* Responses also accepts store (ZDR). Union keeps both families assignable.
*/
type XaiBuiltProviderOptions = XaiProviderOptions & Pick<XaiResponsesProviderOptions, "store">;

/**
* Provider-specific options structure for AI SDK
*/
Expand All @@ -82,7 +92,7 @@ type ProviderOptions =
| { google: GoogleGenerativeAIProviderOptions }
| { openrouter: OpenRouterReasoningOptions }
| { moonshotai: MoonshotAIProviderOptions }
| { xai: XaiProviderOptions }
| { xai: XaiBuiltProviderOptions }
| { "github-copilot": OpenAICompatibleGatewayProviderOptions }
| Record<string, never>; // Empty object for unsupported providers

Expand Down Expand Up @@ -545,6 +555,7 @@ export function buildProviderOptions(
const {
serviceTier: _serviceTier,
searchParameters,
store,
...overrides
} = muxProviderOptions?.xai ?? {};
const isGrok45 = isGrok45Model(capabilityModel);
Expand All @@ -565,13 +576,17 @@ export function buildProviderOptions(
xai: {
...overrides,
...(reasoningEffort != null && { reasoningEffort }),
// ZDR: store:false is required for ZDR orgs. @ai-sdk/xai also auto-adds
// include=["reasoning.encrypted_content"] so reasoning can round-trip
// without server-side response storage (parity with non-ZDR quality).
...(store != null && { store }),
// Grok 4.5 uses xAI's modern Responses tools; getToolsForModel translates
// legacy Live Search settings instead of sending deprecated search_parameters.
...(!isGrok45 && {
searchParameters: searchParameters ?? defaultSearchParameters,
}),
},
} satisfies { xai: XaiProviderOptions };
} satisfies { xai: XaiBuiltProviderOptions };
log.debug("buildProviderOptions: Returning xAI options", options);
return options;
}
Expand Down
Loading
Loading