diff --git a/README.md b/README.md index d960d93fa1..21ea27dee3 100644 --- a/README.md +++ b/README.md @@ -630,8 +630,10 @@ Sub2API supports Grok subscription accounts through xAI OAuth and forwards OpenA - Public Claude-compatible target: `/v1/messages`, converted to xAI Responses and returned as Anthropic Messages output for Claude CLI style clients - Public Chat Completions targets: `/v1/chat/completions` and `/chat/completions`, forwarded to `${XAI_BASE_URL:-https://api.x.ai/v1}/chat/completions` - Codex CLI style Responses WebSocket ingress is accepted on the Responses targets and bridged to xAI HTTP/SSE Responses upstream -- Initial models: `grok-4.3`, `grok-build-0.1`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`, and `grok-4.20-multi-agent-0309` -- Out of scope for this provider: image, video, TTS, transcription, browser automation, cookies, and Grok web scraping +- Initial text models: `grok-4.3`, `grok-build-0.1`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning`, and `grok-4.20-multi-agent-0309` +- Media targets for Grok groups: `/v1/images/generations`, `/images/generations`, `/v1/images/edits`, `/images/edits`, `/v1/videos/generations`, `/videos/generations`, `/v1/videos/{request_id}`, and `/videos/{request_id}`. Generation requests require the group image-generation permission. +- Media models: `grok-imagine`, `grok-imagine-image-quality`, `grok-imagine-image`, `grok-imagine-edit`, `grok-imagine-video`, and `grok-imagine-video-1.5` +- Out of scope for this provider: TTS, transcription, browser automation, cookies, and Grok web scraping ### OAuth Configuration diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index b5f97753d8..cf65d96709 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -419,9 +419,9 @@ - +
@@ -3244,8 +3244,8 @@ const syncFormFromAccount = (newAccount: Account | null) => { : 'https://api.anthropic.com' editBaseUrl.value = platformDefaultUrl - // Load model mappings for OpenAI OAuth accounts - if (newAccount.platform === 'openai' && newAccount.credentials) { + // Load model mappings for OpenAI/Grok OAuth accounts + if ((newAccount.platform === 'openai' || newAccount.platform === 'grok') && newAccount.credentials) { const oauthCredentials = newAccount.credentials as Record loadModelRestrictionFromMapping(oauthCredentials.model_mapping as Record | undefined) } else { @@ -3972,14 +3972,23 @@ const handleSubmit = async () => { updatePayload.credentials = newCredentials } - // OpenAI OAuth: persist model mapping to credentials - if (props.account.platform === 'openai' && props.account.type === 'oauth') { + // OpenAI/Grok OAuth: persist model mapping to credentials + if ((props.account.platform === 'openai' || props.account.platform === 'grok') && props.account.type === 'oauth') { const currentCredentials = isSparkShadow.value ? {} : (updatePayload.credentials as Record) || ((props.account.credentials as Record) || {}) const newCredentials: Record = { ...currentCredentials } - applyOpenAIModelMappingCredentials(newCredentials) + if (props.account.platform === 'openai') { + applyOpenAIModelMappingCredentials(newCredentials) + } else { + const modelMapping = buildModelRestrictionMapping() + if (modelMapping) { + newCredentials.model_mapping = modelMapping + } else { + delete newCredentials.model_mapping + } + } updatePayload.credentials = newCredentials } diff --git a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts index 381d512ee7..5cc22a2958 100644 --- a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts +++ b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts @@ -241,6 +241,32 @@ function buildAntigravityAccount(projectId = 'configured-project') { } as any } +function buildGrokOAuthAccount() { + return { + id: 5, + name: 'Grok OAuth', + notes: '', + platform: 'grok', + type: 'oauth', + credentials: { + refresh_token: 'grok-rt', + base_url: 'https://api.x.ai/v1', + model_mapping: { + 'grok-latest': 'grok-4.3' + } + }, + extra: {}, + proxy_id: null, + concurrency: 1, + priority: 1, + rate_multiplier: 1, + status: 'active', + group_ids: [], + expires_at: null, + auto_pause_on_expired: false + } as any +} + function buildOpenAISetupTokenAccount() { return { ...buildAccount(), @@ -357,6 +383,35 @@ describe('EditAccountModal', () => { }) }) + it('loads and submits Grok OAuth model mapping edits', async () => { + const account = buildGrokOAuthAccount() + updateAccountMock.mockReset() + checkMixedChannelRiskMock.mockReset() + checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false }) + updateAccountMock.mockResolvedValue(account) + + const wrapper = mountModal(account) + expect(wrapper.text()).toContain('Imagine Image') + expect(wrapper.text()).toContain('Imagine Video') + + const inputWithValue = (value: string) => { + const input = wrapper + .findAll('input') + .find((input) => (input.element as HTMLInputElement).value === value) + expect(input).toBeTruthy() + return input! + } + + await inputWithValue('grok-latest').setValue('grok') + await inputWithValue('grok-4.3').setValue('grok-build-0.1') + await wrapper.get('form#edit-account-form').trigger('submit.prevent') + + expect(updateAccountMock).toHaveBeenCalledTimes(1) + expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.model_mapping).toEqual({ + grok: 'grok-build-0.1' + }) + }) + it('only submits model mapping credentials when saving an OpenAI spark shadow account', async () => { authIsSimpleMode.value = false const account = buildOpenAISparkShadowAccount() diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 43449b09ee..654e1bb973 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -141,7 +141,13 @@ const xaiModels = [ 'grok-latest', 'grok-build', 'grok-4.20-reasoning', - 'grok-4.20-non-reasoning' + 'grok-4.20-non-reasoning', + 'grok-imagine', + 'grok-imagine-image-quality', + 'grok-imagine-image', + 'grok-imagine-edit', + 'grok-imagine-video', + 'grok-imagine-video-1.5' ] // Cohere @@ -286,7 +292,10 @@ const grokPresetMappings = [ { label: 'Grok Latest', from: 'grok-latest', to: 'grok-4.3', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, { label: 'Build 0.1', from: 'grok-build', to: 'grok-build-0.1', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: '4.20 Reasoning', from: 'grok-4.20-reasoning', to: 'grok-4.20-0309-reasoning', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' }, - { label: '4.20 Non Reasoning', from: 'grok-4.20-non-reasoning', to: 'grok-4.20-0309-non-reasoning', color: 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' } + { label: '4.20 Non Reasoning', from: 'grok-4.20-non-reasoning', to: 'grok-4.20-0309-non-reasoning', color: 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' }, + { label: 'Imagine Image', from: 'grok-imagine', to: 'grok-imagine-image-quality', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' }, + { label: 'Imagine Edit', from: 'grok-imagine-edit', to: 'grok-imagine-edit', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' }, + { label: 'Imagine Video', from: 'grok-imagine-video-1.5', to: 'grok-imagine-video-1.5', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' } ] // Antigravity 预设映射(支持通配符)