Merge pull request #3673 from heathermhuang/codex/grok-admin-issues-3545-3649

fix: preserve configured Grok OAuth concurrency
This commit is contained in:
Wesley Liddick
2026-07-04 09:22:18 +08:00
committed by GitHub
6 changed files with 91 additions and 26 deletions
+4 -2
View File
@@ -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
@@ -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))
}
@@ -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
}
@@ -419,9 +419,9 @@
</div>
<!-- OpenAI OAuth Model Mapping (OAuth 类型没有 apikey 容器,需要独立的模型映射区域) -->
<!-- OpenAI/Grok OAuth Model Mapping (OAuth 类型没有 apikey 容器,需要独立的模型映射区域) -->
<div
v-if="account.platform === 'openai' && account.type === 'oauth'"
v-if="(account.platform === 'openai' || account.platform === 'grok') && account.type === 'oauth'"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
@@ -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<string, unknown>
loadModelRestrictionFromMapping(oauthCredentials.model_mapping as Record<string, unknown> | 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<string, unknown>) ||
((props.account.credentials as Record<string, unknown>) || {})
const newCredentials: Record<string, unknown> = { ...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
}
@@ -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()
+11 -2
View File
@@ -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 预设映射(支持通配符)