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/backend/internal/service/admin_account_concurrency_test.go b/backend/internal/service/admin_account_concurrency_test.go
index 3544f80e24..da57b5a64f 100644
--- a/backend/internal/service/admin_account_concurrency_test.go
+++ b/backend/internal/service/admin_account_concurrency_test.go
@@ -5,23 +5,16 @@ package service
import (
"testing"
- "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
"github.com/stretchr/testify/require"
)
-func TestNormalizeAccountConcurrencyCapsGrokOAuthUnlessUnsafe(t *testing.T) {
- t.Setenv(xai.EnvUnsafeAllowHighConcurrency, "")
-
+func TestNormalizeAccountConcurrencyDefaultsInvalidGrokOAuthToOne(t *testing.T) {
require.Equal(t, 1, normalizeAccountConcurrency(PlatformGrok, AccountTypeOAuth, 0))
require.Equal(t, 1, normalizeAccountConcurrency(PlatformGrok, AccountTypeOAuth, -5))
- require.Equal(t, 1, normalizeAccountConcurrency(PlatformGrok, AccountTypeOAuth, 50))
+}
+
+func TestNormalizeAccountConcurrencyPreservesExplicitValues(t *testing.T) {
+ require.Equal(t, 50, normalizeAccountConcurrency(PlatformGrok, AccountTypeOAuth, 50))
require.Equal(t, 2, normalizeAccountConcurrency(PlatformOpenAI, AccountTypeOAuth, 2))
require.Equal(t, 2, normalizeAccountConcurrency(PlatformGrok, AccountTypeAPIKey, 2))
}
-
-func TestNormalizeAccountConcurrencyAllowsGrokOAuthUnsafeOverride(t *testing.T) {
- t.Setenv(xai.EnvUnsafeAllowHighConcurrency, "true")
-
- require.Equal(t, 50, normalizeAccountConcurrency(PlatformGrok, AccountTypeOAuth, 50))
- require.Equal(t, 1, normalizeAccountConcurrency(PlatformGrok, AccountTypeOAuth, 0))
-}
diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go
index bacd134db4..7685174f3b 100644
--- a/backend/internal/service/admin_service.go
+++ b/backend/internal/service/admin_service.go
@@ -2640,9 +2640,6 @@ func normalizeAccountConcurrency(platform, accountType string, concurrency int)
if concurrency <= 0 {
return 1
}
- if concurrency > 1 && !xai.AllowUnsafeHighConcurrency() {
- return 1
- }
}
return concurrency
}
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 预设映射(支持通配符)