diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index c365196abcd..d297cc49e3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -33,6 +33,7 @@ import { SerperIcon, TogetherIcon, WizaIcon, + xAIIcon, ZeroBounceIcon, } from '@/components/icons' import { MAX_BYOK_KEYS_PER_PROVIDER } from '@/lib/api/contracts/byok-keys' @@ -75,6 +76,13 @@ const PROVIDERS: (BYOKManagerProvider & { id: BYOKProviderId })[] = [ description: 'LLM calls and Knowledge Base OCR', placeholder: 'Enter your API key', }, + { + id: 'xai', + name: 'xAI', + icon: xAIIcon, + description: 'LLM calls', + placeholder: 'xai-...', + }, { id: 'fireworks', name: 'Fireworks', @@ -279,6 +287,7 @@ const PROVIDER_SECTIONS: BYOKProviderSection[] = [ 'anthropic', 'google', 'mistral', + 'xai', 'fireworks', 'together', 'baseten', diff --git a/apps/sim/executor/handlers/pi/keys.test.ts b/apps/sim/executor/handlers/pi/keys.test.ts index 17b407cc0ad..17c45a6d6ef 100644 --- a/apps/sim/executor/handlers/pi/keys.test.ts +++ b/apps/sim/executor/handlers/pi/keys.test.ts @@ -134,6 +134,21 @@ describe('resolvePiModelKey', () => { expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled() }) + it('cloud mode falls back to a stored workspace key for xAI', async () => { + mockGetProviderFromModel.mockReturnValue('xai') + mockGetBYOKKey.mockResolvedValue({ apiKey: 'xai-workspace-key', isBYOK: true }) + + const result = await resolvePiModelKey({ + model: 'grok-4.5', + mode: 'cloud', + workspaceId: 'ws-1', + }) + + expect(result).toEqual({ providerId: 'xai', apiKey: 'xai-workspace-key', isBYOK: true }) + expect(mockGetBYOKKey).toHaveBeenCalledWith('ws-1', 'xai') + expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled() + }) + it('cloud mode rejects when no user key is available (never a hosted key)', async () => { mockGetProviderFromModel.mockReturnValue('anthropic') mockGetBYOKKey.mockResolvedValue(null) diff --git a/apps/sim/executor/handlers/pi/keys.ts b/apps/sim/executor/handlers/pi/keys.ts index 9d85eb8a4ee..cc28d6de4b4 100644 --- a/apps/sim/executor/handlers/pi/keys.ts +++ b/apps/sim/executor/handlers/pi/keys.ts @@ -33,7 +33,13 @@ interface ResolvePiModelKeyParams { } /** Providers whose key Sim can store as a workspace BYOK key (read back for cloud). */ -const WORKSPACE_BYOK_PROVIDERS = new Set(['anthropic', 'openai', 'google', 'mistral']) +const WORKSPACE_BYOK_PROVIDERS = new Set([ + 'anthropic', + 'openai', + 'google', + 'mistral', + 'xai', +]) /** Resolves the provider and a usable API key for the selected model. */ export async function resolvePiModelKey(params: ResolvePiModelKeyParams): Promise { diff --git a/apps/sim/lib/api-key/byok.ts b/apps/sim/lib/api-key/byok.ts index 4a4f26b3271..6e45fe2cad8 100644 --- a/apps/sim/lib/api-key/byok.ts +++ b/apps/sim/lib/api-key/byok.ts @@ -205,13 +205,14 @@ export async function getApiKeyWithBYOK( const isGeminiModel = provider === 'google' const isMistralModel = provider === 'mistral' const isZaiModel = provider === 'zai' + const isXaiModel = provider === 'xai' const byokProviderId = isGeminiModel ? 'google' : (provider as BYOKProviderId) if ( isHosted && workspaceId && - (isOpenAIModel || isClaudeModel || isGeminiModel || isMistralModel || isZaiModel) + (isOpenAIModel || isClaudeModel || isGeminiModel || isMistralModel || isZaiModel || isXaiModel) ) { const hostedModels = getHostedModels() const isModelHosted = hostedModels.some((m) => m.toLowerCase() === model.toLowerCase()) diff --git a/apps/sim/lib/api/contracts/byok-keys.ts b/apps/sim/lib/api/contracts/byok-keys.ts index d9b8a561099..80e7b075df1 100644 --- a/apps/sim/lib/api/contracts/byok-keys.ts +++ b/apps/sim/lib/api/contracts/byok-keys.ts @@ -7,6 +7,7 @@ export const byokProviderIdSchema = z.enum([ 'google', 'mistral', 'zai', + 'xai', 'fireworks', 'together', 'baseten', diff --git a/apps/sim/lib/core/config/api-keys.ts b/apps/sim/lib/core/config/api-keys.ts index 1a1f04218cd..b435da46f22 100644 --- a/apps/sim/lib/core/config/api-keys.ts +++ b/apps/sim/lib/core/config/api-keys.ts @@ -12,7 +12,8 @@ export function getRotatingApiKey(provider: string): string { provider !== 'anthropic' && provider !== 'gemini' && provider !== 'cohere' && - provider !== 'zai' + provider !== 'zai' && + provider !== 'xai' ) { throw new Error(`No rotation implemented for provider: ${provider}`) } @@ -39,6 +40,10 @@ export function getRotatingApiKey(provider: string): string { if (env.ZAI_API_KEY_1) keys.push(env.ZAI_API_KEY_1) if (env.ZAI_API_KEY_2) keys.push(env.ZAI_API_KEY_2) if (env.ZAI_API_KEY_3) keys.push(env.ZAI_API_KEY_3) + } else if (provider === 'xai') { + if (env.XAI_API_KEY_1) keys.push(env.XAI_API_KEY_1) + if (env.XAI_API_KEY_2) keys.push(env.XAI_API_KEY_2) + if (env.XAI_API_KEY_3) keys.push(env.XAI_API_KEY_3) } if (keys.length === 0) { diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 6cf92e243a3..0be542a4f4c 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -147,6 +147,9 @@ export const env = createEnv({ ZAI_API_KEY_1: z.string().min(1).optional(), // Primary Z.ai API key for load balancing ZAI_API_KEY_2: z.string().min(1).optional(), // Additional Z.ai API key for load balancing ZAI_API_KEY_3: z.string().min(1).optional(), // Additional Z.ai API key for load balancing + XAI_API_KEY_1: z.string().min(1).optional(), // Primary xAI API key for load balancing + XAI_API_KEY_2: z.string().min(1).optional(), // Additional xAI API key for load balancing + XAI_API_KEY_3: z.string().min(1).optional(), // Additional xAI API key for load balancing OLLAMA_URL: z.string().url().optional(), // Ollama local LLM server URL VLLM_BASE_URL: z.string().url().optional(), // vLLM self-hosted base URL (OpenAI-compatible) VLLM_API_KEY: z.string().optional(), // Optional bearer token for vLLM diff --git a/apps/sim/lib/core/utils.test.ts b/apps/sim/lib/core/utils.test.ts index a9e2b1662f9..7ccb5dc75cf 100644 --- a/apps/sim/lib/core/utils.test.ts +++ b/apps/sim/lib/core/utils.test.ts @@ -43,6 +43,9 @@ vi.mock('@/lib/core/config/env', () => GEMINI_API_KEY_1: 'test-gemini-key-1', GEMINI_API_KEY_2: 'test-gemini-key-2', GEMINI_API_KEY_3: 'test-gemini-key-3', + XAI_API_KEY_1: 'test-xai-key-1', + XAI_API_KEY_2: 'test-xai-key-2', + XAI_API_KEY_3: 'test-xai-key-3', }) ) @@ -327,6 +330,11 @@ describe('getRotatingApiKey', () => { expect(result).toMatch(/^test-gemini-key-[1-3]$/) }) + it.concurrent('should return xAI API key based on current minute', () => { + const result = getRotatingApiKey('xai') + expect(result).toMatch(/^test-xai-key-[1-3]$/) + }) + it.concurrent('should throw error for unsupported provider', () => { expect(() => getRotatingApiKey('unsupported')).toThrow('No rotation implemented for provider') }) diff --git a/apps/sim/providers/models.test.ts b/apps/sim/providers/models.test.ts index 0c01133649c..14dcee48590 100644 --- a/apps/sim/providers/models.test.ts +++ b/apps/sim/providers/models.test.ts @@ -218,3 +218,17 @@ describe('zai provider definition', () => { expect(getHostedModels()).toContain('glm-4.6') }) }) + +describe('xai provider definition', () => { + const xai = PROVIDER_DEFINITIONS.xai + + it('is registered with grok-4.5 as the default model', () => { + expect(xai).toBeDefined() + expect(xai.id).toBe('xai') + expect(xai.defaultModel).toBe('grok-4.5') + }) + + it('is included in getHostedModels since Sim provides the xAI key server-side', () => { + expect(getHostedModels()).toContain('grok-4.5') + }) +}) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index e5021a4c721..bc5aa24cbe7 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -3921,6 +3921,7 @@ export function getHostedModels(): string[] { ...getProviderModels('anthropic'), ...getProviderModels('google'), ...getProviderModels('zai'), + ...getProviderModels('xai'), ] } diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index 1e9c877a976..1697c12f80d 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -865,7 +865,7 @@ describe('Cost Calculation', () => { }) describe('getHostedModels', () => { - it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => { + it.concurrent('should return OpenAI, Anthropic, Google, and xAI models as hosted', () => { const hostedModels = getHostedModels() expect(hostedModels).toContain('gpt-4o') @@ -877,8 +877,9 @@ describe('getHostedModels', () => { expect(hostedModels).toContain('gemini-2.5-pro') expect(hostedModels).toContain('gemini-2.5-flash') + expect(hostedModels).toContain('grok-4.5') + expect(hostedModels).not.toContain('deepseek-v3') - expect(hostedModels).not.toContain('grok-4-latest') }) it.concurrent('should return an array of strings', () => { @@ -902,11 +903,12 @@ describe('shouldBillModelUsage', () => { expect(shouldBillModelUsage('gemini-2.5-pro')).toBe(true) expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true) + + expect(shouldBillModelUsage('grok-4.5')).toBe(true) }) it.concurrent('should return false for non-hosted models', () => { expect(shouldBillModelUsage('deepseek-v3')).toBe(false) - expect(shouldBillModelUsage('grok-4-latest')).toBe(false) expect(shouldBillModelUsage('unknown-model')).toBe(false) }) diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 3b8bdd10c6a..8ca3017354a 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -903,8 +903,9 @@ export function getApiKey(provider: string, model: string, userProvidedKey?: str const isClaudeModel = provider === 'anthropic' const isGeminiModel = provider === 'google' const isZaiModel = provider === 'zai' + const isXaiModel = provider === 'xai' - if (isHosted && (isOpenAIModel || isClaudeModel || isGeminiModel || isZaiModel)) { + if (isHosted && (isOpenAIModel || isClaudeModel || isGeminiModel || isZaiModel || isXaiModel)) { const hostedModels = getHostedModels() const isModelHosted = hostedModels.some((m) => m.toLowerCase() === model.toLowerCase()) diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index 910e825f251..cb3e7753a1d 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -8,6 +8,7 @@ export type BYOKProviderId = | 'google' | 'mistral' | 'zai' + | 'xai' | 'fireworks' | 'together' | 'baseten'