diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue
index 6734031950..f6d941ea4a 100644
--- a/frontend/src/components/account/BulkEditAccountModal.vue
+++ b/frontend/src/components/account/BulkEditAccountModal.vue
@@ -538,75 +538,10 @@
{{ t('admin.accounts.headerOverride.bulkReplaceHint') }}
-
-
-
-
-
-
-
-
-
-
- {{ t('admin.accounts.headerOverride.emptyValueHint') }}
-
+
{{ t('admin.accounts.headerOverride.bulkDisableHint') }}
@@ -1277,17 +1212,15 @@ import {
buildModelMappingObject as buildModelMappingPayload,
getPresetMappingsByPlatform
} from '@/composables/useModelWhitelist'
-import HeaderOverrideJsonTools from '@/components/account/HeaderOverrideJsonTools.vue'
+import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue'
import {
buildHeaderOverridesObject,
- getHeaderOverrideTemplate,
isHeaderOverrideCapable,
validateHeaderOverrideRows,
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
HEADER_OVERRIDES_CREDENTIAL_KEY,
type HeaderOverrideRow
} from '@/components/account/credentialsBuilder'
-import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
import {
OPENAI_WS_MODE_CTX_POOL,
OPENAI_WS_MODE_OFF,
@@ -1436,37 +1369,6 @@ const customErrorCodeInput = ref(null)
const interceptWarmupRequests = ref(false)
const headerOverrideEnabled = ref(false)
const headerOverrideRows = ref([])
-const getHeaderOverrideRowKey = createStableObjectKeyResolver('bulk-header-override-row')
-
-const addHeaderOverrideRow = () => {
- headerOverrideRows.value.push({ name: '', value: '' })
-}
-
-const removeHeaderOverrideRow = (index: number) => {
- headerOverrideRows.value.splice(index, 1)
-}
-
-// 模板仅在所选账号平台唯一时可用:混合 anthropic+openai 选择无法确定用哪套模板,
-// 误填会把另一平台的专有头写进所有所选账号
-const headerOverrideTemplatePlatform = computed(() => {
- return targetSelectedPlatforms.value.length === 1 ? targetSelectedPlatforms.value[0] : null
-})
-
-// 模板按钮:填入所选平台的标准客户端请求头名称(值留空),跳过已存在的同名行
-const fillHeaderOverrideTemplate = () => {
- const platform = headerOverrideTemplatePlatform.value
- if (!platform) return
- const existing = new Set(
- headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean)
- )
- const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim())
- for (const row of getHeaderOverrideTemplate(platform)) {
- if (!existing.has(row.name)) {
- rows.push(row)
- }
- }
- headerOverrideRows.value = rows
-}
const proxyId = ref(null)
const concurrency = ref(1)
const loadFactor = ref(null)
diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue
index 6132a1f1e1..5c21f95fd3 100644
--- a/frontend/src/components/account/CreateAccountModal.vue
+++ b/frontend/src/components/account/CreateAccountModal.vue
@@ -1533,74 +1533,10 @@
-
-
-
-
-
-
-
-
-
-
- {{ t('admin.accounts.headerOverride.emptyValueHint') }}
-
+
@@ -1968,6 +1904,90 @@
/>
+
+
+
+
+
+
+ {{ t('admin.accounts.grokCustomBaseUrl.hint') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('admin.accounts.headerOverride.hint') }}
+
+
+
+
+
+
+
+
+
+ {{ t('admin.accounts.headerOverride.info') }}
+
+
+
+
+
+
+
(null)
const headerOverrideEnabled = ref(false)
const headerOverrideRows = ref([])
-const addHeaderOverrideRow = () => {
- headerOverrideRows.value.push({ name: '', value: '' })
-}
+// Grok OAuth:自定义上游地址(base_url 仅改写转发端点,OAuth 授权/刷新不受影响)
+const grokOAuthCustomBaseUrlEnabled = ref(false)
+const grokOAuthBaseUrl = ref('')
-const removeHeaderOverrideRow = (index: number) => {
- headerOverrideRows.value.splice(index, 1)
-}
-
-// 模板按钮:填入标准客户端请求头名称(值留空),跳过已存在的同名行
-const fillHeaderOverrideTemplate = () => {
- const existing = new Set(
- headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean)
- )
- const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim())
- for (const row of getHeaderOverrideTemplate(form.platform)) {
- if (!existing.has(row.name)) {
- rows.push(row)
+// Grok OAuth 三条创建路径(授权码/RT 批量/SSO 批量)共用的前置校验。
+// 授权码路径必须在兑换 code 之前调用,避免校验失败时白白消耗一次性授权码。
+const validateGrokOAuthUpstreamConfig = (): boolean => {
+ if (grokOAuthCustomBaseUrlEnabled.value) {
+ const trimmed = grokOAuthBaseUrl.value.trim()
+ if (!trimmed) {
+ appStore.showError(t('admin.accounts.grokCustomBaseUrl.required'))
+ return false
+ }
+ if (!/^https?:\/\//i.test(trimmed)) {
+ appStore.showError(t('admin.accounts.grokCustomBaseUrl.invalid'))
+ return false
}
}
- headerOverrideRows.value = rows
+ if (headerOverrideEnabled.value) {
+ const headerError = validateHeaderOverrideRows(headerOverrideRows.value)
+ if (headerError) {
+ appStore.showError(t(`admin.accounts.headerOverride.${headerError}`))
+ return false
+ }
+ }
+ return true
+}
+
+// 把已通过校验的自定义上游地址与请求头覆写写入 credentials
+const applyGrokOAuthUpstreamConfig = (credentials: Record) => {
+ if (grokOAuthCustomBaseUrlEnabled.value) {
+ credentials.base_url = grokOAuthBaseUrl.value.trim()
+ }
+ applyHeaderOverride(credentials, headerOverrideEnabled.value, headerOverrideRows.value, 'create')
}
const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(true)
@@ -3780,7 +3813,6 @@ const vertexServiceAccountDragActive = ref(false)
const tempUnschedEnabled = ref(false)
const tempUnschedRules = ref([])
const getModelMappingKey = createStableObjectKeyResolver('create-model-mapping')
-const getHeaderOverrideRowKey = createStableObjectKeyResolver('create-header-override-row')
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver('create-openai-compact-model-mapping')
const getAntigravityModelMappingKey = createStableObjectKeyResolver('create-antigravity-model-mapping')
const getTempUnschedRuleKey = createStableObjectKeyResolver('create-temp-unsched-rule')
@@ -4184,10 +4216,12 @@ watch(
anthropicAPIKeyAuthScheme.value = 'x_api_key'
webSearchEmulationMode.value = 'default'
}
- // 请求头覆写为平台相关配置(模板/常用头集合不同),切换平台时清空,
- // 避免上一平台的模板行被提交到新平台账号
+ // 请求头覆写为平台相关配置(常用头集合不同),切换平台时清空,
+ // 避免上一平台的配置行被提交到新平台账号
headerOverrideEnabled.value = false
headerOverrideRows.value = []
+ grokOAuthCustomBaseUrlEnabled.value = false
+ grokOAuthBaseUrl.value = ''
// Reset OAuth states
oauth.resetState()
openaiOAuth.resetState()
@@ -4579,6 +4613,8 @@ const resetForm = () => {
customErrorCodeInput.value = null
headerOverrideEnabled.value = false
headerOverrideRows.value = []
+ grokOAuthCustomBaseUrlEnabled.value = false
+ grokOAuthBaseUrl.value = ''
interceptWarmupRequests.value = false
autoPauseOnExpired.value = true
openaiPassthroughEnabled.value = false
@@ -5194,6 +5230,7 @@ const handleGrokValidateRT = async (refreshTokenInput: string) => {
grokOAuth.error.value = t('admin.accounts.oauth.grok.pleaseEnterRefreshToken')
return
}
+ if (!validateGrokOAuthUpstreamConfig()) return
grokOAuth.loading.value = true
grokOAuth.error.value = ''
@@ -5214,6 +5251,7 @@ const handleGrokValidateRT = async (refreshTokenInput: string) => {
}
const credentials = grokOAuth.buildCredentials(tokenInfo)
+ applyGrokOAuthUpstreamConfig(credentials)
const extra = grokOAuth.buildExtraInfo(tokenInfo)
const accountName = refreshTokens.length > 1 ? `${form.name || tokenInfo.email || 'Grok OAuth Account'} #${i + 1}` : (form.name || tokenInfo.email || 'Grok OAuth Account')
@@ -5277,11 +5315,13 @@ const handleGrokImportSSO = async (ssoInput: string) => {
.map((token) => token.trim())
.filter((token) => token)
if (ssoTokens.length === 0) return
+ if (!validateGrokOAuthUpstreamConfig()) return
grokOAuth.loading.value = true
grokOAuth.error.value = ''
const credentials: Record = {}
+ applyGrokOAuthUpstreamConfig(credentials)
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
if (modelMapping) {
credentials.model_mapping = modelMapping
@@ -5904,6 +5944,7 @@ const handleAntigravityExchange = async (authCode: string) => {
// Grok OAuth 授权码兑换
const handleGrokExchange = async (authCode: string) => {
if (!authCode.trim() || !grokOAuth.sessionId.value) return
+ if (!validateGrokOAuthUpstreamConfig()) return
grokOAuth.loading.value = true
grokOAuth.error.value = ''
@@ -5926,6 +5967,7 @@ const handleGrokExchange = async (authCode: string) => {
if (!tokenInfo) return
const credentials = grokOAuth.buildCredentials(tokenInfo)
+ applyGrokOAuthUpstreamConfig(credentials)
const extra = grokOAuth.buildExtraInfo(tokenInfo)
await createAccountAndFinish('grok', 'oauth', credentials, extra)
} catch (error: any) {
diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue
index 12dd469c65..418fb62d88 100644
--- a/frontend/src/components/account/EditAccountModal.vue
+++ b/frontend/src/components/account/EditAccountModal.vue
@@ -497,74 +497,10 @@
-
-
-
-
-
-
-
-
-
-
- {{ t('admin.accounts.headerOverride.emptyValueHint') }}
-
+
@@ -2630,7 +2566,7 @@ import ProxyAdBanner from '@/components/common/ProxyAdBanner.vue'
import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
-import HeaderOverrideJsonTools from '@/components/account/HeaderOverrideJsonTools.vue'
+import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue'
import {
applyAntigravityProjectID,
applyHeaderOverride,
@@ -2638,7 +2574,6 @@ import {
applyPlanType,
buildPlanTypeOptions,
readPlanType,
- getHeaderOverrideTemplate,
isCustomGrokBaseUrl,
isHeaderOverrideCapable,
splitHeaderOverridesObject,
@@ -2788,27 +2723,6 @@ const headerOverrideCapable = computed(
const grokOAuthCustomBaseUrlEnabled = ref(false)
const grokOAuthBaseUrl = ref('')
-const addHeaderOverrideRow = () => {
- headerOverrideRows.value.push({ name: '', value: '' })
-}
-
-const removeHeaderOverrideRow = (index: number) => {
- headerOverrideRows.value.splice(index, 1)
-}
-
-// 模板按钮:填入标准客户端请求头名称(值留空),跳过已存在的同名行
-const fillHeaderOverrideTemplate = () => {
- const existing = new Set(
- headerOverrideRows.value.map((row) => row.name.trim().toLowerCase()).filter(Boolean)
- )
- const rows = headerOverrideRows.value.filter((row) => row.name.trim() || row.value.trim())
- for (const row of getHeaderOverrideTemplate(props.account?.platform || '')) {
- if (!existing.has(row.name)) {
- rows.push(row)
- }
- }
- headerOverrideRows.value = rows
-}
const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(false)
const autoPause5hThreshold = ref(null)
@@ -2825,7 +2739,6 @@ const isSyncingAntigravityUpstream = ref(false)
const tempUnschedEnabled = ref(false)
const tempUnschedRules = ref([])
const getModelMappingKey = createStableObjectKeyResolver('edit-model-mapping')
-const getHeaderOverrideRowKey = createStableObjectKeyResolver('edit-header-override-row')
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver('edit-openai-compact-model-mapping')
const getAntigravityModelMappingKey = createStableObjectKeyResolver('edit-antigravity-model-mapping')
const getTempUnschedRuleKey = createStableObjectKeyResolver('edit-temp-unsched-rule')
diff --git a/frontend/src/components/account/HeaderOverrideEditor.vue b/frontend/src/components/account/HeaderOverrideEditor.vue
new file mode 100644
index 0000000000..d84bbf6192
--- /dev/null
+++ b/frontend/src/components/account/HeaderOverrideEditor.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t('admin.accounts.headerOverride.emptyValueHint') }}
+
+
+
+
diff --git a/frontend/src/components/account/HeaderOverrideJsonTools.vue b/frontend/src/components/account/HeaderOverrideJsonTools.vue
index 04391a62a3..f2afad2860 100644
--- a/frontend/src/components/account/HeaderOverrideJsonTools.vue
+++ b/frontend/src/components/account/HeaderOverrideJsonTools.vue
@@ -15,12 +15,13 @@
{{ t('admin.accounts.headerOverride.copyJson') }}
-