From 2f937f2fdbba7f41bbecbd08acd19b86dd0f080c Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Sun, 19 Jul 2026 16:21:51 +0800 Subject: [PATCH] Add xAI API provider support Register xAI as a built-in OpenAI-compatible chat provider and offer current Grok 4.5 and Grok 4.3 presets. Wire shared API-key storage, request routing, setup links, and focused coverage so the provider behaves like existing built-in adapters. --- src/background/index.mjs | 2 ++ src/config/index.mjs | 19 +++++++++++++++++++ src/config/openai-provider-mappings.mjs | 2 ++ src/popup/sections/GeneralPart.jsx | 2 ++ src/services/apis/provider-registry.mjs | 9 +++++++++ tests/unit/config/config-predicates.test.mjs | 9 +++++++++ .../services/apis/provider-registry.test.mjs | 5 +++++ .../unit/services/apis/thin-adapters.test.mjs | 7 +++++++ 8 files changed, 55 insertions(+) diff --git a/src/background/index.mjs b/src/background/index.mjs index 89a1ea048..bcc0566e9 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -31,6 +31,7 @@ import { isUsingAimlApiModel, isUsingDeepSeekApiModel, isUsingGoogleApiModel, + isUsingXaiApiModel, } from '../config/index.mjs' import '../_locales/i18n' import { openUrl } from '../utils/open-url' @@ -402,6 +403,7 @@ function isUsingOpenAICompatibleApiSession(session) { isUsingOpenRouterApiModel(session) || isUsingAimlApiModel(session) || isUsingGoogleApiModel(session) || + isUsingXaiApiModel(session) || isUsingGptCompletionApiModel(session) ) } diff --git a/src/config/index.mjs b/src/config/index.mjs index e1a22f822..b21a9d0cb 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -187,6 +187,7 @@ export const googleApiModelKeys = [ 'googleGemini2_5Flash', 'googleGemini2_5FlashLite', ] +export const xaiApiModelKeys = ['xaiGrok4_5', 'xaiGrok4_3'] export const AlwaysCustomGroups = [ 'ollamaApiModelKeys', @@ -265,6 +266,10 @@ export const ModelGroups = { value: googleApiModelKeys, desc: 'Google (API)', }, + xaiApiModelKeys: { + value: xaiApiModelKeys, + desc: 'xAI (API)', + }, customApiModelKeys: { value: customApiModelKeys, desc: 'Custom Model', @@ -641,6 +646,14 @@ export const Models = { value: 'gemini-2.5-flash-lite', desc: 'Google (Gemini 2.5 Flash-Lite)', }, + xaiGrok4_5: { + value: 'grok-4.5', + desc: 'xAI (Grok 4.5)', + }, + xaiGrok4_3: { + value: 'grok-4.3', + desc: 'xAI (Grok 4.3)', + }, } for (const modelName in Models) { @@ -710,6 +723,7 @@ export const defaultConfig = { openRouterApiKey: '', aimlApiKey: '', googleApiKey: '', + xaiApiKey: '', // advanced @@ -745,6 +759,7 @@ export const defaultConfig = { 'chatgptApi5_6Sol', 'chatgptApi5_6Terra', 'chatgptApi5_6Luna', + 'xaiGrok4_5', 'claudeOpus48Api', 'claudeSonnet5Api', 'claudeHaiku45Api', @@ -911,6 +926,10 @@ export function isUsingGoogleApiModel(configOrSession) { return isInApiModeGroup(googleApiModelKeys, configOrSession) } +export function isUsingXaiApiModel(configOrSession) { + return isInApiModeGroup(xaiApiModelKeys, configOrSession) +} + export function isUsingChatGLMApiModel(configOrSession) { return isInApiModeGroup(chatglmApiModelKeys, configOrSession) } diff --git a/src/config/openai-provider-mappings.mjs b/src/config/openai-provider-mappings.mjs index 57c0485a3..6f2a6a4ff 100644 --- a/src/config/openai-provider-mappings.mjs +++ b/src/config/openai-provider-mappings.mjs @@ -7,6 +7,7 @@ export const LEGACY_API_KEY_FIELD_BY_PROVIDER_ID = { chatglm: 'chatglmApiKey', ollama: 'ollamaApiKey', google: 'googleApiKey', + xai: 'xaiApiKey', 'legacy-custom-default': 'customApiKey', } @@ -28,5 +29,6 @@ export const OPENAI_COMPATIBLE_GROUP_TO_PROVIDER_ID = { chatglmApiModelKeys: 'chatglm', ollamaApiModelKeys: 'ollama', googleApiModelKeys: 'google', + xaiApiModelKeys: 'xai', customApiModelKeys: 'legacy-custom-default', } diff --git a/src/popup/sections/GeneralPart.jsx b/src/popup/sections/GeneralPart.jsx index bf1798b89..a30e73d69 100644 --- a/src/popup/sections/GeneralPart.jsx +++ b/src/popup/sections/GeneralPart.jsx @@ -60,6 +60,8 @@ function getProviderApiKeySetupUrl(providerId) { switch (String(providerId || '').trim()) { case 'openai': return 'https://platform.openai.com/account/api-keys' + case 'xai': + return 'https://console.x.ai/team/default/api-keys' case 'moonshot': return 'https://platform.moonshot.cn/console/api-keys' default: diff --git a/src/services/apis/provider-registry.mjs b/src/services/apis/provider-registry.mjs index 72aa47662..f443b3eaf 100644 --- a/src/services/apis/provider-registry.mjs +++ b/src/services/apis/provider-registry.mjs @@ -78,6 +78,14 @@ const BUILTIN_PROVIDER_TEMPLATE = [ builtin: true, enabled: true, }, + { + id: 'xai', + name: 'xAI', + baseUrl: 'https://api.x.ai/v1', + chatCompletionsPath: '/chat/completions', + builtin: true, + enabled: true, + }, { id: 'legacy-custom-default', name: 'Custom Model (Legacy)', @@ -120,6 +128,7 @@ function resolveProviderIdFromLegacyModelName(modelName) { } if (preset.startsWith('chatglm') || preset === 'chatglmApiModelKeys') return 'chatglm' if (preset.startsWith('googleGemini') || preset === 'googleApiModelKeys') return 'google' + if (preset.startsWith('xaiGrok') || preset === 'xaiApiModelKeys') return 'xai' if (preset === 'customApiModelKeys') return 'legacy-custom-default' return null diff --git a/tests/unit/config/config-predicates.test.mjs b/tests/unit/config/config-predicates.test.mjs index a899f31df..77445f4dc 100644 --- a/tests/unit/config/config-predicates.test.mjs +++ b/tests/unit/config/config-predicates.test.mjs @@ -9,6 +9,7 @@ import { claudeApiModelKeys, openRouterApiModelKeys, aimlApiModelKeys, + xaiApiModelKeys, isUsingAimlApiModel, isUsingAzureOpenAiApiModel, isUsingBingWebModel, @@ -28,6 +29,7 @@ import { isUsingOpenAiApiModel, isUsingGptCompletionApiModel, isUsingOpenRouterApiModel, + isUsingXaiApiModel, } from '../../../src/config/index.mjs' import { LEGACY_MODEL_KEY_MIGRATIONS, @@ -242,6 +244,13 @@ test('isUsingOpenRouterApiModel accepts exported OpenRouter API model keys', () } }) +test('isUsingXaiApiModel accepts exported xAI API model keys', () => { + for (const modelName of xaiApiModelKeys) { + assert.equal(isUsingXaiApiModel({ modelName }), true) + } + assert.equal(isUsingXaiApiModel({ modelName: 'chatgptApi4oMini' }), false) +}) + test('isUsingAimlApiModel matches representative AI/ML API keys', () => { for (const modelName of representativeAimlApiModelNames) { assert.equal(isUsingAimlApiModel({ modelName }), true) diff --git a/tests/unit/services/apis/provider-registry.test.mjs b/tests/unit/services/apis/provider-registry.test.mjs index 29d949814..003a9de07 100644 --- a/tests/unit/services/apis/provider-registry.test.mjs +++ b/tests/unit/services/apis/provider-registry.test.mjs @@ -2578,6 +2578,11 @@ test('resolveOpenAICompatibleRequest avoids duplicate /v1 for custom provider ba assert.equal(resolved.requestUrl, 'https://proxy.example.com/v1/chat/completions') }) +test('resolveProviderIdForSession resolves legacy xAI model names to xai provider', () => { + assert.equal(resolveProviderIdForSession({ modelName: 'xaiGrok4_5' }), 'xai') + assert.equal(resolveProviderIdForSession({ modelName: 'xaiApiModelKeys-custom' }), 'xai') +}) + test('resolveOpenAICompatibleRequest preserves /v1 for custom provider baseUrl with explicit non-default paths', () => { const config = { customOpenAIProviders: [ diff --git a/tests/unit/services/apis/thin-adapters.test.mjs b/tests/unit/services/apis/thin-adapters.test.mjs index 385fd25f7..8a01e9874 100644 --- a/tests/unit/services/apis/thin-adapters.test.mjs +++ b/tests/unit/services/apis/thin-adapters.test.mjs @@ -63,6 +63,13 @@ const adapters = [ expectedBaseUrl: 'https://open.bigmodel.cn/api/paas/v4', expectedApiKey: 'glm-key', }, + { + name: 'xai-api', + apiMode: { groupName: 'xaiApiModelKeys', itemName: 'xaiGrok4_5' }, + providerId: 'xai', + expectedBaseUrl: 'https://api.x.ai/v1', + expectedApiKey: 'xai-key', + }, ] for (const adapter of adapters) {