Skip to content

Commit 7c2de1d

Browse files
authored
feat(providers): add xAI to hosted key rotation pool (#5574)
* feat(providers): add xAI to hosted key rotation pool Wires xai into the same hosted-key mechanism as OpenAI, Anthropic, and Z.ai so Sim can serve Grok models without users bringing their own key. * fix(pi): include xai in Pi cloud-mode workspace BYOK read-back xai was fully wired as a Pi-supported provider but missing from WORKSPACE_BYOK_PROVIDERS, so a stored workspace xAI key was never read back for cloud-mode Pi runs. * fix(byok): add xai settings UI row xai is both hosted (Pi block hides its inline API key field for hosted models) and Pi-supported (cloud mode requires a user key), so without a Settings > BYOK row users had no way to supply an xai key for Pi cloud runs.
1 parent def2d52 commit 7c2de1d

13 files changed

Lines changed: 74 additions & 7 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
SerperIcon,
3535
TogetherIcon,
3636
WizaIcon,
37+
xAIIcon,
3738
ZeroBounceIcon,
3839
} from '@/components/icons'
3940
import { MAX_BYOK_KEYS_PER_PROVIDER } from '@/lib/api/contracts/byok-keys'
@@ -76,6 +77,13 @@ const PROVIDERS: (BYOKManagerProvider & { id: BYOKProviderId })[] = [
7677
description: 'LLM calls and Knowledge Base OCR',
7778
placeholder: 'Enter your API key',
7879
},
80+
{
81+
id: 'xai',
82+
name: 'xAI',
83+
icon: xAIIcon,
84+
description: 'LLM calls',
85+
placeholder: 'xai-...',
86+
},
7987
{
8088
id: 'fireworks',
8189
name: 'Fireworks',
@@ -287,6 +295,7 @@ const PROVIDER_SECTIONS: BYOKProviderSection[] = [
287295
'anthropic',
288296
'google',
289297
'mistral',
298+
'xai',
290299
'fireworks',
291300
'together',
292301
'baseten',

apps/sim/executor/handlers/pi/keys.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ describe('resolvePiModelKey', () => {
134134
expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled()
135135
})
136136

137+
it('cloud mode falls back to a stored workspace key for xAI', async () => {
138+
mockGetProviderFromModel.mockReturnValue('xai')
139+
mockGetBYOKKey.mockResolvedValue({ apiKey: 'xai-workspace-key', isBYOK: true })
140+
141+
const result = await resolvePiModelKey({
142+
model: 'grok-4.5',
143+
mode: 'cloud',
144+
workspaceId: 'ws-1',
145+
})
146+
147+
expect(result).toEqual({ providerId: 'xai', apiKey: 'xai-workspace-key', isBYOK: true })
148+
expect(mockGetBYOKKey).toHaveBeenCalledWith('ws-1', 'xai')
149+
expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled()
150+
})
151+
137152
it('cloud mode rejects when no user key is available (never a hosted key)', async () => {
138153
mockGetProviderFromModel.mockReturnValue('anthropic')
139154
mockGetBYOKKey.mockResolvedValue(null)

apps/sim/executor/handlers/pi/keys.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ interface ResolvePiModelKeyParams {
3333
}
3434

3535
/** Providers whose key Sim can store as a workspace BYOK key (read back for cloud). */
36-
const WORKSPACE_BYOK_PROVIDERS = new Set<string>(['anthropic', 'openai', 'google', 'mistral'])
36+
const WORKSPACE_BYOK_PROVIDERS = new Set<string>([
37+
'anthropic',
38+
'openai',
39+
'google',
40+
'mistral',
41+
'xai',
42+
])
3743

3844
/** Resolves the provider and a usable API key for the selected model. */
3945
export async function resolvePiModelKey(params: ResolvePiModelKeyParams): Promise<PiKeyResolution> {

apps/sim/lib/api-key/byok.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,13 +205,14 @@ export async function getApiKeyWithBYOK(
205205
const isGeminiModel = provider === 'google'
206206
const isMistralModel = provider === 'mistral'
207207
const isZaiModel = provider === 'zai'
208+
const isXaiModel = provider === 'xai'
208209

209210
const byokProviderId = isGeminiModel ? 'google' : (provider as BYOKProviderId)
210211

211212
if (
212213
isHosted &&
213214
workspaceId &&
214-
(isOpenAIModel || isClaudeModel || isGeminiModel || isMistralModel || isZaiModel)
215+
(isOpenAIModel || isClaudeModel || isGeminiModel || isMistralModel || isZaiModel || isXaiModel)
215216
) {
216217
const hostedModels = getHostedModels()
217218
const isModelHosted = hostedModels.some((m) => m.toLowerCase() === model.toLowerCase())

apps/sim/lib/api/contracts/byok-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const byokProviderIdSchema = z.enum([
77
'google',
88
'mistral',
99
'zai',
10+
'xai',
1011
'fireworks',
1112
'together',
1213
'baseten',

apps/sim/lib/core/config/api-keys.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export function getRotatingApiKey(provider: string): string {
1212
provider !== 'anthropic' &&
1313
provider !== 'gemini' &&
1414
provider !== 'cohere' &&
15-
provider !== 'zai'
15+
provider !== 'zai' &&
16+
provider !== 'xai'
1617
) {
1718
throw new Error(`No rotation implemented for provider: ${provider}`)
1819
}
@@ -39,6 +40,10 @@ export function getRotatingApiKey(provider: string): string {
3940
if (env.ZAI_API_KEY_1) keys.push(env.ZAI_API_KEY_1)
4041
if (env.ZAI_API_KEY_2) keys.push(env.ZAI_API_KEY_2)
4142
if (env.ZAI_API_KEY_3) keys.push(env.ZAI_API_KEY_3)
43+
} else if (provider === 'xai') {
44+
if (env.XAI_API_KEY_1) keys.push(env.XAI_API_KEY_1)
45+
if (env.XAI_API_KEY_2) keys.push(env.XAI_API_KEY_2)
46+
if (env.XAI_API_KEY_3) keys.push(env.XAI_API_KEY_3)
4247
}
4348

4449
if (keys.length === 0) {

apps/sim/lib/core/config/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ export const env = createEnv({
147147
ZAI_API_KEY_1: z.string().min(1).optional(), // Primary Z.ai API key for load balancing
148148
ZAI_API_KEY_2: z.string().min(1).optional(), // Additional Z.ai API key for load balancing
149149
ZAI_API_KEY_3: z.string().min(1).optional(), // Additional Z.ai API key for load balancing
150+
XAI_API_KEY_1: z.string().min(1).optional(), // Primary xAI API key for load balancing
151+
XAI_API_KEY_2: z.string().min(1).optional(), // Additional xAI API key for load balancing
152+
XAI_API_KEY_3: z.string().min(1).optional(), // Additional xAI API key for load balancing
150153
OLLAMA_URL: z.string().url().optional(), // Ollama local LLM server URL
151154
VLLM_BASE_URL: z.string().url().optional(), // vLLM self-hosted base URL (OpenAI-compatible)
152155
VLLM_API_KEY: z.string().optional(), // Optional bearer token for vLLM

apps/sim/lib/core/utils.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ vi.mock('@/lib/core/config/env', () =>
4343
GEMINI_API_KEY_1: 'test-gemini-key-1',
4444
GEMINI_API_KEY_2: 'test-gemini-key-2',
4545
GEMINI_API_KEY_3: 'test-gemini-key-3',
46+
XAI_API_KEY_1: 'test-xai-key-1',
47+
XAI_API_KEY_2: 'test-xai-key-2',
48+
XAI_API_KEY_3: 'test-xai-key-3',
4649
})
4750
)
4851

@@ -327,6 +330,11 @@ describe('getRotatingApiKey', () => {
327330
expect(result).toMatch(/^test-gemini-key-[1-3]$/)
328331
})
329332

333+
it.concurrent('should return xAI API key based on current minute', () => {
334+
const result = getRotatingApiKey('xai')
335+
expect(result).toMatch(/^test-xai-key-[1-3]$/)
336+
})
337+
330338
it.concurrent('should throw error for unsupported provider', () => {
331339
expect(() => getRotatingApiKey('unsupported')).toThrow('No rotation implemented for provider')
332340
})

apps/sim/providers/models.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,17 @@ describe('zai provider definition', () => {
218218
expect(getHostedModels()).toContain('glm-4.6')
219219
})
220220
})
221+
222+
describe('xai provider definition', () => {
223+
const xai = PROVIDER_DEFINITIONS.xai
224+
225+
it('is registered with grok-4.5 as the default model', () => {
226+
expect(xai).toBeDefined()
227+
expect(xai.id).toBe('xai')
228+
expect(xai.defaultModel).toBe('grok-4.5')
229+
})
230+
231+
it('is included in getHostedModels since Sim provides the xAI key server-side', () => {
232+
expect(getHostedModels()).toContain('grok-4.5')
233+
})
234+
})

apps/sim/providers/models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3921,6 +3921,7 @@ export function getHostedModels(): string[] {
39213921
...getProviderModels('anthropic'),
39223922
...getProviderModels('google'),
39233923
...getProviderModels('zai'),
3924+
...getProviderModels('xai'),
39243925
]
39253926
}
39263927

0 commit comments

Comments
 (0)