Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
c7e18bd5
Unverified
Commit
c7e18bd5
authored
Feb 25, 2026
by
Wesley Liddick
Committed by
GitHub
Feb 25, 2026
Browse files
Merge pull request #627 from touwaeriol/pr/bugfixes-and-enhancements
feat: 反重力(Antigravity)增强、Failover 重构及新模型支持
parents
516f8f28
8365a832
Changes
41
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/antigravity_model_mapping_test.go
View file @
c7e18bd5
...
...
@@ -76,6 +76,12 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) {
},
// 3. 默认映射中的透传(映射到自己)
{
name
:
"默认映射透传 - claude-sonnet-4-6"
,
requestedModel
:
"claude-sonnet-4-6"
,
accountMapping
:
nil
,
expected
:
"claude-sonnet-4-6"
,
},
{
name
:
"默认映射透传 - claude-sonnet-4-5"
,
requestedModel
:
"claude-sonnet-4-5"
,
...
...
backend/internal/service/antigravity_rate_limit_test.go
View file @
c7e18bd5
...
...
@@ -197,6 +197,22 @@ func TestHandleUpstreamError_429_NonModelRateLimit(t *testing.T) {
require
.
Equal
(
t
,
"claude-sonnet-4-5"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
}
// TestHandleUpstreamError_429_NonModelRateLimit_UsesMappedModelKey 测试 429 非模型限流场景
// 验证:requestedModel 会被映射到 Antigravity 最终模型(例如 claude-opus-4-6 -> claude-opus-4-6-thinking)
func
TestHandleUpstreamError_429_NonModelRateLimit_UsesMappedModelKey
(
t
*
testing
.
T
)
{
repo
:=
&
stubAntigravityAccountRepo
{}
svc
:=
&
AntigravityGatewayService
{
accountRepo
:
repo
}
account
:=
&
Account
{
ID
:
20
,
Name
:
"acc-20"
,
Platform
:
PlatformAntigravity
}
body
:=
buildGeminiRateLimitBody
(
"5s"
)
result
:=
svc
.
handleUpstreamError
(
context
.
Background
(),
"[test]"
,
account
,
http
.
StatusTooManyRequests
,
http
.
Header
{},
body
,
"claude-opus-4-6"
,
0
,
""
,
false
)
require
.
Nil
(
t
,
result
)
require
.
Len
(
t
,
repo
.
modelRateLimitCalls
,
1
)
require
.
Equal
(
t
,
"claude-opus-4-6-thinking"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
}
// TestHandleUpstreamError_503_ModelCapacityExhausted 测试 503 模型容量不足场景
// MODEL_CAPACITY_EXHAUSTED 时应等待重试,不切换账号
func
TestHandleUpstreamError_503_ModelCapacityExhausted
(
t
*
testing
.
T
)
{
...
...
backend/internal/service/billing_service.go
View file @
c7e18bd5
...
...
@@ -133,6 +133,18 @@ func (s *BillingService) initFallbackPricing() {
CacheReadPricePerToken
:
0.03e-6
,
// $0.03 per MTok
SupportsCacheBreakdown
:
false
,
}
// Claude 4.6 Opus (与4.5同价)
s
.
fallbackPrices
[
"claude-opus-4.6"
]
=
s
.
fallbackPrices
[
"claude-opus-4.5"
]
// Gemini 3.1 Pro
s
.
fallbackPrices
[
"gemini-3.1-pro"
]
=
&
ModelPricing
{
InputPricePerToken
:
2e-6
,
// $2 per MTok
OutputPricePerToken
:
12e-6
,
// $12 per MTok
CacheCreationPricePerToken
:
2e-6
,
// $2 per MTok
CacheReadPricePerToken
:
0.2e-6
,
// $0.20 per MTok
SupportsCacheBreakdown
:
false
,
}
}
// getFallbackPricing 根据模型系列获取回退价格
...
...
@@ -141,6 +153,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
// 按模型系列匹配
if
strings
.
Contains
(
modelLower
,
"opus"
)
{
if
strings
.
Contains
(
modelLower
,
"4.6"
)
||
strings
.
Contains
(
modelLower
,
"4-6"
)
{
return
s
.
fallbackPrices
[
"claude-opus-4.6"
]
}
if
strings
.
Contains
(
modelLower
,
"4.5"
)
||
strings
.
Contains
(
modelLower
,
"4-5"
)
{
return
s
.
fallbackPrices
[
"claude-opus-4.5"
]
}
...
...
@@ -158,6 +173,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
}
return
s
.
fallbackPrices
[
"claude-3-haiku"
]
}
if
strings
.
Contains
(
modelLower
,
"gemini-3.1-pro"
)
||
strings
.
Contains
(
modelLower
,
"gemini-3-1-pro"
)
{
return
s
.
fallbackPrices
[
"gemini-3.1-pro"
]
}
// 默认使用Sonnet价格
return
s
.
fallbackPrices
[
"claude-sonnet-4"
]
...
...
backend/internal/service/gateway_multiplatform_test.go
View file @
c7e18bd5
...
...
@@ -895,6 +895,55 @@ func TestGatewayService_SelectAccountForModelWithPlatform_GeminiPreferOAuth(t *t
require
.
Equal
(
t
,
int64
(
2
),
acc
.
ID
)
}
func
TestGatewayService_SelectAccountForModelWithPlatform_GeminiAPIKeyModelMappingFilter
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
repo
:=
&
mockAccountRepoForPlatform
{
accounts
:
[]
Account
{
{
ID
:
1
,
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Priority
:
1
,
Status
:
StatusActive
,
Schedulable
:
true
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-pro"
:
"gemini-2.5-pro"
}},
},
{
ID
:
2
,
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Priority
:
2
,
Status
:
StatusActive
,
Schedulable
:
true
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-flash"
:
"gemini-2.5-flash"
}},
},
},
accountsByID
:
map
[
int64
]
*
Account
{},
}
for
i
:=
range
repo
.
accounts
{
repo
.
accountsByID
[
repo
.
accounts
[
i
]
.
ID
]
=
&
repo
.
accounts
[
i
]
}
cache
:=
&
mockGatewayCacheForPlatform
{}
svc
:=
&
GatewayService
{
accountRepo
:
repo
,
cache
:
cache
,
cfg
:
testConfig
(),
}
acc
,
err
:=
svc
.
selectAccountForModelWithPlatform
(
ctx
,
nil
,
""
,
"gemini-2.5-flash"
,
nil
,
PlatformGemini
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
acc
)
require
.
Equal
(
t
,
int64
(
2
),
acc
.
ID
,
"应过滤不支持请求模型的 APIKey 账号"
)
acc
,
err
=
svc
.
selectAccountForModelWithPlatform
(
ctx
,
nil
,
""
,
"gemini-3-pro-preview"
,
nil
,
PlatformGemini
)
require
.
Error
(
t
,
err
)
require
.
Nil
(
t
,
acc
)
require
.
Contains
(
t
,
err
.
Error
(),
"supporting model"
)
}
func
TestGatewayService_SelectAccountForModelWithPlatform_StickyInGroup
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
groupID
:=
int64
(
50
)
...
...
@@ -1070,6 +1119,36 @@ func TestGatewayService_isModelSupportedByAccount(t *testing.T) {
model
:
"claude-3-5-sonnet-20241022"
,
expected
:
true
,
},
{
name
:
"Gemini平台-无映射配置-支持所有模型"
,
account
:
&
Account
{
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
},
model
:
"gemini-2.5-flash"
,
expected
:
true
,
},
{
name
:
"Gemini平台-有映射配置-只支持配置的模型"
,
account
:
&
Account
{
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-pro"
:
"gemini-2.5-pro"
},
},
},
model
:
"gemini-2.5-flash"
,
expected
:
false
,
},
{
name
:
"Gemini平台-有映射配置-支持配置的模型"
,
account
:
&
Account
{
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-pro"
:
"gemini-2.5-pro"
},
},
},
model
:
"gemini-2.5-pro"
,
expected
:
true
,
},
}
for
_
,
tt
:=
range
tests
{
...
...
backend/internal/service/gateway_service.go
View file @
c7e18bd5
...
...
@@ -2825,10 +2825,6 @@ func (s *GatewayService) isModelSupportedByAccount(account *Account, requestedMo
if
account
.
Platform
==
PlatformAnthropic
&&
account
.
Type
!=
AccountTypeAPIKey
{
requestedModel
=
claude
.
NormalizeModelID
(
requestedModel
)
}
// Gemini API Key 账户直接透传,由上游判断模型是否支持
if
account
.
Platform
==
PlatformGemini
&&
account
.
Type
==
AccountTypeAPIKey
{
return
true
}
// 其他平台使用账户的模型支持检查
return
account
.
IsModelSupported
(
requestedModel
)
}
...
...
backend/internal/service/model_rate_limit_test.go
View file @
c7e18bd5
...
...
@@ -107,12 +107,12 @@ func TestIsModelRateLimited(t *testing.T) {
expected
:
true
,
},
{
name
:
"antigravity platform - gemini-3-pro-preview mapped to gemini-3
.1
-pro-high"
,
name
:
"antigravity platform - gemini-3-pro-preview mapped to gemini-3-pro-high"
,
account
:
&
Account
{
Platform
:
PlatformAntigravity
,
Extra
:
map
[
string
]
any
{
modelRateLimitsKey
:
map
[
string
]
any
{
"gemini-3
.1
-pro-high"
:
map
[
string
]
any
{
"gemini-3-pro-high"
:
map
[
string
]
any
{
"rate_limit_reset_at"
:
future
,
},
},
...
...
backend/migrations/058_add_sonnet46_to_model_mapping.sql
0 → 100644
View file @
c7e18bd5
-- Add claude-sonnet-4-6 to model_mapping for all Antigravity accounts
--
-- Background:
-- Antigravity now supports claude-sonnet-4-6
--
-- Strategy:
-- Directly overwrite the entire model_mapping with updated mappings
-- This ensures consistency with DefaultAntigravityModelMapping in constants.go
UPDATE
accounts
SET
credentials
=
jsonb_set
(
credentials
,
'{model_mapping}'
,
'{
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-6": "claude-opus-4-6-thinking",
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking",
"claude-sonnet-4-6": "claude-sonnet-4-6",
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
"claude-haiku-4-5": "claude-sonnet-4-5",
"claude-haiku-4-5-20251001": "claude-sonnet-4-5",
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
"gemini-2.5-pro": "gemini-2.5-pro",
"gemini-3-flash": "gemini-3-flash",
"gemini-3-pro-high": "gemini-3-pro-high",
"gemini-3-pro-low": "gemini-3-pro-low",
"gemini-3-pro-image": "gemini-3-pro-image",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-preview": "gemini-3-pro-high",
"gemini-3-pro-image-preview": "gemini-3-pro-image",
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
"tab_flash_lite_preview": "tab_flash_lite_preview"
}'
::
jsonb
)
WHERE
platform
=
'antigravity'
AND
deleted_at
IS
NULL
AND
credentials
->
'model_mapping'
IS
NOT
NULL
;
backend/migrations/059_add_gemini31_pro_to_model_mapping.sql
0 → 100644
View file @
c7e18bd5
-- Add gemini-3.1-pro-high, gemini-3.1-pro-low, gemini-3.1-pro-preview to model_mapping
--
-- Background:
-- Antigravity now supports gemini-3.1-pro-high and gemini-3.1-pro-low
--
-- Strategy:
-- Directly overwrite the entire model_mapping with updated mappings
-- This ensures consistency with DefaultAntigravityModelMapping in constants.go
UPDATE
accounts
SET
credentials
=
jsonb_set
(
credentials
,
'{model_mapping}'
,
'{
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-6": "claude-opus-4-6-thinking",
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking",
"claude-sonnet-4-6": "claude-sonnet-4-6",
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
"claude-haiku-4-5": "claude-sonnet-4-5",
"claude-haiku-4-5-20251001": "claude-sonnet-4-5",
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
"gemini-2.5-pro": "gemini-2.5-pro",
"gemini-3-flash": "gemini-3-flash",
"gemini-3-pro-high": "gemini-3-pro-high",
"gemini-3-pro-low": "gemini-3-pro-low",
"gemini-3-pro-image": "gemini-3-pro-image",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-preview": "gemini-3-pro-high",
"gemini-3-pro-image-preview": "gemini-3-pro-image",
"gemini-3.1-pro-high": "gemini-3.1-pro-high",
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
"gemini-3.1-pro-preview": "gemini-3.1-pro-high",
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
"tab_flash_lite_preview": "tab_flash_lite_preview"
}'
::
jsonb
)
WHERE
platform
=
'antigravity'
AND
deleted_at
IS
NULL
AND
credentials
->
'model_mapping'
IS
NOT
NULL
;
frontend/src/api/admin/accounts.ts
View file @
c7e18bd5
...
...
@@ -15,7 +15,9 @@ import type {
AccountUsageStatsResponse
,
TempUnschedulableStatus
,
AdminDataPayload
,
AdminDataImportResult
AdminDataImportResult
,
CheckMixedChannelRequest
,
CheckMixedChannelResponse
}
from
'
@/types
'
/**
...
...
@@ -133,6 +135,16 @@ export async function update(id: number, updates: UpdateAccountRequest): Promise
return
data
}
/**
* Check mixed-channel risk for account-group binding.
*/
export
async
function
checkMixedChannelRisk
(
payload
:
CheckMixedChannelRequest
):
Promise
<
CheckMixedChannelResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
CheckMixedChannelResponse
>
(
'
/admin/accounts/check-mixed-channel
'
,
payload
)
return
data
}
/**
* Delete account
* @param id - Account ID
...
...
@@ -535,6 +547,7 @@ export const accountsAPI = {
getById
,
create
,
update
,
checkMixedChannelRisk
,
delete
:
deleteAccount
,
toggleStatus
,
testAccount
,
...
...
frontend/src/components/account/AccountStatusIndicator.vue
View file @
c7e18bd5
...
...
@@ -77,13 +77,23 @@
</div>
<!-- Model Rate Limit Indicators (Antigravity OAuth Smart Retry) -->
<
template
v-if=
"activeModelRateLimits.length > 0"
>
<div
v-for=
"item in activeModelRateLimits"
:key=
"item.model"
class=
"group relative"
>
<div
v-if=
"activeModelRateLimits.length > 0"
:class=
"[
activeModelRateLimits.length <= 4
? 'flex flex-col gap-1'
: activeModelRateLimits.length <= 8
? 'columns-2 gap-x-2'
: 'columns-3 gap-x-2'
]"
>
<div
v-for=
"item in activeModelRateLimits"
:key=
"item.model"
class=
"group relative mb-1 break-inside-avoid"
>
<span
class=
"inline-flex items-center gap-1 rounded bg-purple-100 px-1.5 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
>
<Icon
name=
"exclamationTriangle"
size=
"xs"
:stroke-width=
"2"
/>
{{ formatScopeName(item.model) }}
<span
class=
"text-[10px] opacity-70"
>
{{ formatModelResetTime(item.reset_at) }}
</span>
</span>
<!-- Tooltip -->
<div
...
...
@@ -95,7 +105,7 @@
></div>
</div>
</div>
<
/
template
>
</
div
>
<!-- Overload Indicator (529) -->
<div
v-if=
"isOverloaded"
class=
"group relative"
>
...
...
@@ -154,17 +164,50 @@ const activeModelRateLimits = computed(() => {
})
const
formatScopeName
=
(
scope
:
string
):
string
=>
{
const
names
:
Record
<
string
,
string
>
=
{
const
aliases
:
Record
<
string
,
string
>
=
{
// Claude 系列
'
claude-opus-4-6-thinking
'
:
'
COpus46
'
,
'
claude-sonnet-4-6
'
:
'
CSon46
'
,
'
claude-sonnet-4-5
'
:
'
CSon45
'
,
'
claude-sonnet-4-5-thinking
'
:
'
CSon45T
'
,
// Gemini 2.5 系列
'
gemini-2.5-flash
'
:
'
G25F
'
,
'
gemini-2.5-flash-lite
'
:
'
G25FL
'
,
'
gemini-2.5-flash-thinking
'
:
'
G25FT
'
,
'
gemini-2.5-pro
'
:
'
G25P
'
,
// Gemini 3 系列
'
gemini-3-flash
'
:
'
G3F
'
,
'
gemini-3.1-pro-high
'
:
'
G3PH
'
,
'
gemini-3.1-pro-low
'
:
'
G3PL
'
,
'
gemini-3-pro-image
'
:
'
G3PI
'
,
// 其他
'
gpt-oss-120b-medium
'
:
'
GPT120
'
,
'
tab_flash_lite_preview
'
:
'
TabFL
'
,
// 旧版 scope 别名(兼容)
claude
:
'
Claude
'
,
claude_sonnet
:
'
C
laude Sonnet
'
,
claude_opus
:
'
C
laude
Opus
'
,
claude_haiku
:
'
C
laude
Haiku
'
,
claude_sonnet
:
'
C
Son
'
,
claude_opus
:
'
COpus
'
,
claude_haiku
:
'
CHaiku
'
,
gemini_text
:
'
Gemini
'
,
gemini_image
:
'
Im
age
'
,
gemini_flash
:
'
G
emini
Flash
'
,
gemini_pro
:
'
G
emini
Pro
'
gemini_image
:
'
G
Im
g
'
,
gemini_flash
:
'
GFlash
'
,
gemini_pro
:
'
GPro
'
,
}
return
names
[
scope
]
||
scope
return
aliases
[
scope
]
||
scope
}
const
formatModelResetTime
=
(
resetAt
:
string
):
string
=>
{
const
date
=
new
Date
(
resetAt
)
const
now
=
new
Date
()
const
diffMs
=
date
.
getTime
()
-
now
.
getTime
()
if
(
diffMs
<=
0
)
return
''
const
totalSecs
=
Math
.
floor
(
diffMs
/
1000
)
const
h
=
Math
.
floor
(
totalSecs
/
3600
)
const
m
=
Math
.
floor
((
totalSecs
%
3600
)
/
60
)
const
s
=
totalSecs
%
60
if
(
h
>
0
)
return
`
${
h
}
h
${
m
}
m`
if
(
m
>
0
)
return
`
${
m
}
m
${
s
}
s`
return
`
${
s
}
s`
}
// Computed: is overloaded (529)
...
...
frontend/src/components/account/AccountUsageCell.vue
View file @
c7e18bd5
...
...
@@ -172,12 +172,12 @@
color=
"purple"
/>
<!-- Claude
4.5
-->
<!-- Claude -->
<UsageProgressBar
v-if=
"antigravityClaude
45
UsageFromAPI !== null"
:label=
"t('admin.accounts.usageWindow.claude
45
')"
:utilization=
"antigravityClaude
45
UsageFromAPI.utilization"
:resets-at=
"antigravityClaude
45
UsageFromAPI.resetTime"
v-if=
"antigravityClaudeUsageFromAPI !== null"
:label=
"t('admin.accounts.usageWindow.claude')"
:utilization=
"antigravityClaudeUsageFromAPI.utilization"
:resets-at=
"antigravityClaudeUsageFromAPI.resetTime"
color=
"amber"
/>
</div>
...
...
@@ -400,9 +400,12 @@ const antigravity3FlashUsageFromAPI = computed(() => getAntigravityUsageFromAPI(
// Gemini 3 Image from API
const
antigravity3ImageUsageFromAPI
=
computed
(()
=>
getAntigravityUsageFromAPI
([
'
gemini-3-pro-image
'
]))
// Claude 4.5 from API
const
antigravityClaude45UsageFromAPI
=
computed
(()
=>
getAntigravityUsageFromAPI
([
'
claude-sonnet-4-5
'
,
'
claude-opus-4-5-thinking
'
])
// Claude from API (all Claude model variants)
const
antigravityClaudeUsageFromAPI
=
computed
(()
=>
getAntigravityUsageFromAPI
([
'
claude-sonnet-4-5
'
,
'
claude-opus-4-5-thinking
'
,
'
claude-sonnet-4-6
'
,
'
claude-opus-4-6-thinking
'
,
])
)
// Antigravity 账户类型(从 load_code_assist 响应中提取)
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
c7e18bd5
...
...
@@ -209,7 +209,7 @@
<
div
v
-
if
=
"
modelMappings.length > 0
"
class
=
"
mb-3 space-y-2
"
>
<
div
v
-
for
=
"
(mapping, index) in modelMappings
"
:
key
=
"
getModelMappingKey(mapping)
"
:
key
=
"
index
"
class
=
"
flex items-center gap-2
"
>
<
input
...
...
@@ -654,7 +654,7 @@ import Select from '@/components/common/Select.vue'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
buildModelMappingObject
as
buildModelMappingPayload
}
from
'
@/composables/useModelWhitelist
'
interface
Props
{
show
:
boolean
...
...
@@ -696,7 +696,6 @@ const baseUrl = ref('')
const
modelRestrictionMode
=
ref
<
'
whitelist
'
|
'
mapping
'
>
(
'
whitelist
'
)
const
allowedModels
=
ref
<
string
[]
>
([])
const
modelMappings
=
ref
<
ModelMapping
[]
>
([])
const
getModelMappingKey
=
createStableObjectKeyResolver
<
ModelMapping
>
(
'
bulk-model-mapping
'
)
const
selectedErrorCodes
=
ref
<
number
[]
>
([])
const
customErrorCodeInput
=
ref
<
number
|
null
>
(
null
)
const
interceptWarmupRequests
=
ref
(
false
)
...
...
@@ -707,7 +706,7 @@ const rateMultiplier = ref(1)
const
status
=
ref
<
'
active
'
|
'
inactive
'
>
(
'
active
'
)
const
groupIds
=
ref
<
number
[]
>
([])
// All models list (combined Anthropic + OpenAI)
// All models list (combined Anthropic + OpenAI
+ Gemini
)
const
allModels
=
[
{
value
:
'
claude-opus-4-6
'
,
label
:
'
Claude Opus 4.6
'
}
,
{
value
:
'
claude-sonnet-4-6
'
,
label
:
'
Claude Sonnet 4.6
'
}
,
...
...
@@ -719,17 +718,21 @@ const allModels = [
{
value
:
'
claude-3-opus-20240229
'
,
label
:
'
Claude 3 Opus
'
}
,
{
value
:
'
claude-3-5-sonnet-20241022
'
,
label
:
'
Claude 3.5 Sonnet
'
}
,
{
value
:
'
claude-3-haiku-20240307
'
,
label
:
'
Claude 3 Haiku
'
}
,
{
value
:
'
gpt-5.3-codex-spark
'
,
label
:
'
GPT-5.3 Codex Spark
'
}
,
{
value
:
'
gpt-5.2-2025-12-11
'
,
label
:
'
GPT-5.2
'
}
,
{
value
:
'
gpt-5.2-codex
'
,
label
:
'
GPT-5.2 Codex
'
}
,
{
value
:
'
gpt-5.1-codex-max
'
,
label
:
'
GPT-5.1 Codex Max
'
}
,
{
value
:
'
gpt-5.1-codex
'
,
label
:
'
GPT-5.1 Codex
'
}
,
{
value
:
'
gpt-5.1-2025-11-13
'
,
label
:
'
GPT-5.1
'
}
,
{
value
:
'
gpt-5.1-codex-mini
'
,
label
:
'
GPT-5.1 Codex Mini
'
}
,
{
value
:
'
gpt-5-2025-08-07
'
,
label
:
'
GPT-5
'
}
{
value
:
'
gpt-5-2025-08-07
'
,
label
:
'
GPT-5
'
}
,
{
value
:
'
gemini-2.0-flash
'
,
label
:
'
Gemini 2.0 Flash
'
}
,
{
value
:
'
gemini-2.5-flash
'
,
label
:
'
Gemini 2.5 Flash
'
}
,
{
value
:
'
gemini-2.5-pro
'
,
label
:
'
Gemini 2.5 Pro
'
}
,
{
value
:
'
gemini-3-flash-preview
'
,
label
:
'
Gemini 3 Flash Preview
'
}
,
{
value
:
'
gemini-3-pro-preview
'
,
label
:
'
Gemini 3 Pro Preview
'
}
]
// Preset mappings (combined Anthropic + OpenAI)
// Preset mappings (combined Anthropic + OpenAI
+ Gemini
)
const
presetMappings
=
[
{
label
:
'
Sonnet 4
'
,
...
...
@@ -796,12 +799,6 @@ const presetMappings = [
to
:
'
claude-sonnet-4-5-20250929
'
,
color
:
'
bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400
'
}
,
{
label
:
'
GPT-5.3 Codex Spark
'
,
from
:
'
gpt-5.3-codex-spark
'
,
to
:
'
gpt-5.3-codex-spark
'
,
color
:
'
bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400
'
}
,
{
label
:
'
GPT-5.2
'
,
from
:
'
gpt-5.2-2025-12-11
'
,
...
...
@@ -938,23 +935,11 @@ const removeErrorCode = (code: number) => {
}
const
buildModelMappingObject
=
():
Record
<
string
,
string
>
|
null
=>
{
const
mapping
:
Record
<
string
,
string
>
=
{
}
if
(
modelRestrictionMode
.
value
===
'
whitelist
'
)
{
for
(
const
model
of
allowedModels
.
value
)
{
mapping
[
model
]
=
model
}
}
else
{
for
(
const
m
of
modelMappings
.
value
)
{
const
from
=
m
.
from
.
trim
()
const
to
=
m
.
to
.
trim
()
if
(
from
&&
to
)
{
mapping
[
from
]
=
to
}
}
}
return
Object
.
keys
(
mapping
).
length
>
0
?
mapping
:
null
return
buildModelMappingPayload
(
modelRestrictionMode
.
value
,
allowedModels
.
value
,
modelMappings
.
value
)
}
const
buildUpdatePayload
=
():
Record
<
string
,
unknown
>
|
null
=>
{
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
c7e18bd5
...
...
@@ -916,8 +916,8 @@
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.gemini.tier.aiStudioHint
'
)
}}
</p>
</div>
<!-- Model Restriction Section (
不适用于 Gemini,
Antigravity 已在上层条件排除) -->
<div
v-if=
"form.platform !== 'gemini'"
class=
"border-t border-gray-200 pt-4 dark:border-dark-600"
>
<!-- Model Restriction Section (Antigravity 已在上层条件排除) -->
<div
class=
"border-t border-gray-200 pt-4 dark:border-dark-600"
>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.modelRestriction
'
)
}}
</label>
<div
...
...
@@ -1200,34 +1200,6 @@
<
/div
>
<
/div
>
<!--
Gemini
模型说明
-->
<
div
v
-
if
=
"
form.platform === 'gemini'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20
"
>
<
div
class
=
"
flex items-start gap-3
"
>
<
svg
class
=
"
h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
stroke
-
width
=
"
2
"
d
=
"
M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z
"
/>
<
/svg
>
<
div
>
<
p
class
=
"
text-sm font-medium text-blue-800 dark:text-blue-300
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthrough
'
)
}}
<
/p
>
<
p
class
=
"
mt-1 text-xs text-blue-700 dark:text-blue-400
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthroughDesc
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Temp
Unschedulable
Rules
-->
...
...
@@ -1378,9 +1350,9 @@
<
/div
>
<
/div
>
<!--
Intercept
Warmup
Requests
(
Anthropic
onl
y
)
-->
<!--
Intercept
Warmup
Requests
(
Anthropic
/
Antigravit
y
)
-->
<
div
v
-
if
=
"
form.platform === 'anthropic'
"
v
-
if
=
"
form.platform === 'anthropic'
|| form.platform === 'antigravity'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
...
...
@@ -2157,7 +2129,7 @@
<
ConfirmDialog
:
show
=
"
showMixedChannelWarning
"
:
title
=
"
t('admin.accounts.mixedChannelWarningTitle')
"
:
message
=
"
mixedChannelWarning
Details ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''
"
:
message
=
"
mixedChannelWarning
MessageText
"
:
confirm
-
text
=
"
t('common.confirm')
"
:
cancel
-
text
=
"
t('common.cancel')
"
:
danger
=
"
true
"
...
...
@@ -2189,13 +2161,21 @@ import {
import
{
useOpenAIOAuth
}
from
'
@/composables/useOpenAIOAuth
'
import
{
useGeminiOAuth
}
from
'
@/composables/useGeminiOAuth
'
import
{
useAntigravityOAuth
}
from
'
@/composables/useAntigravityOAuth
'
import
type
{
Proxy
,
AdminGroup
,
AccountPlatform
,
AccountType
}
from
'
@/types
'
import
type
{
Proxy
,
AdminGroup
,
AccountPlatform
,
AccountType
,
CheckMixedChannelResponse
,
CreateAccountRequest
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
ModelWhitelistSelector
from
'
@/components/account/ModelWhitelistSelector.vue
'
import
{
applyInterceptWarmup
}
from
'
@/components/account/credentialsBuilder
'
import
{
formatDateTimeLocalInput
,
parseDateTimeLocalInput
}
from
'
@/utils/format
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
OAuthAuthorizationFlow
from
'
./OAuthAuthorizationFlow.vue
'
...
...
@@ -2337,10 +2317,13 @@ const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>
const
geminiOAuthType
=
ref
<
'
code_assist
'
|
'
google_one
'
|
'
ai_studio
'
>
(
'
google_one
'
)
const
geminiAIStudioOAuthEnabled
=
ref
(
false
)
// Mixed channel warning dialog state
const
showMixedChannelWarning
=
ref
(
false
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
null
)
const
pendingCreatePayload
=
ref
<
any
>
(
null
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
null
)
const
mixedChannelWarningRawMessage
=
ref
(
''
)
const
mixedChannelWarningAction
=
ref
<
(()
=>
Promise
<
void
>
)
|
null
>
(
null
)
const
antigravityMixedChannelConfirmed
=
ref
(
false
)
const
showAdvancedOAuth
=
ref
(
false
)
const
showGeminiHelpDialog
=
ref
(
false
)
...
...
@@ -2378,6 +2361,13 @@ const isOpenAIModelRestrictionDisabled = computed(() =>
form
.
platform
===
'
openai
'
&&
openaiPassthroughEnabled
.
value
)
const
mixedChannelWarningMessageText
=
computed
(()
=>
{
if
(
mixedChannelWarningDetails
.
value
)
{
return
t
(
'
admin.accounts.mixedChannelWarning
'
,
mixedChannelWarningDetails
.
value
)
}
return
mixedChannelWarningRawMessage
.
value
}
)
const
geminiQuotaDocs
=
{
codeAssist
:
'
https://developers.google.com/gemini-code-assist/resources/quotas
'
,
aiStudio
:
'
https://ai.google.dev/pricing
'
,
...
...
@@ -2544,8 +2534,8 @@ watch(
antigravityModelMappings
.
value
=
[]
antigravityModelRestrictionMode
.
value
=
'
mapping
'
}
// Reset Anthropic-specific settings when switching to other platforms
if
(
newPlatform
!==
'
anthropic
'
)
{
// Reset Anthropic
/Antigravity
-specific settings when switching to other platforms
if
(
newPlatform
!==
'
anthropic
'
&&
newPlatform
!==
'
antigravity
'
)
{
interceptWarmupRequests
.
value
=
false
}
if
(
newPlatform
===
'
sora
'
)
{
...
...
@@ -2794,6 +2784,105 @@ const splitTempUnschedKeywords = (value: string) => {
.
filter
((
item
)
=>
item
.
length
>
0
)
}
const
needsMixedChannelCheck
=
(
platform
:
AccountPlatform
)
=>
platform
===
'
antigravity
'
||
platform
===
'
anthropic
'
const
buildMixedChannelDetails
=
(
resp
?:
CheckMixedChannelResponse
)
=>
{
const
details
=
resp
?.
details
if
(
!
details
)
{
return
null
}
return
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
}
const
clearMixedChannelDialog
=
()
=>
{
showMixedChannelWarning
.
value
=
false
mixedChannelWarningDetails
.
value
=
null
mixedChannelWarningRawMessage
.
value
=
''
mixedChannelWarningAction
.
value
=
null
}
const
openMixedChannelDialog
=
(
opts
:
{
response
?:
CheckMixedChannelResponse
message
?:
string
onConfirm
:
()
=>
Promise
<
void
>
}
)
=>
{
mixedChannelWarningDetails
.
value
=
buildMixedChannelDetails
(
opts
.
response
)
mixedChannelWarningRawMessage
.
value
=
opts
.
message
||
opts
.
response
?.
message
||
t
(
'
admin.accounts.failedToCreate
'
)
mixedChannelWarningAction
.
value
=
opts
.
onConfirm
showMixedChannelWarning
.
value
=
true
}
const
withAntigravityConfirmFlag
=
(
payload
:
CreateAccountRequest
):
CreateAccountRequest
=>
{
if
(
needsMixedChannelCheck
(
payload
.
platform
)
&&
antigravityMixedChannelConfirmed
.
value
)
{
return
{
...
payload
,
confirm_mixed_channel_risk
:
true
}
}
const
cloned
=
{
...
payload
}
delete
cloned
.
confirm_mixed_channel_risk
return
cloned
}
const
ensureAntigravityMixedChannelConfirmed
=
async
(
onConfirm
:
()
=>
Promise
<
void
>
):
Promise
<
boolean
>
=>
{
if
(
!
needsMixedChannelCheck
(
form
.
platform
))
{
return
true
}
if
(
antigravityMixedChannelConfirmed
.
value
)
{
return
true
}
try
{
const
result
=
await
adminAPI
.
accounts
.
checkMixedChannelRisk
({
platform
:
form
.
platform
,
group_ids
:
form
.
group_ids
}
)
if
(
!
result
.
has_risk
)
{
return
true
}
openMixedChannelDialog
({
response
:
result
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
onConfirm
()
}
}
)
return
false
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
return
false
}
}
const
submitCreateAccount
=
async
(
payload
:
CreateAccountRequest
)
=>
{
submitting
.
value
=
true
try
{
await
adminAPI
.
accounts
.
create
(
withAntigravityConfirmFlag
(
payload
))
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
emit
(
'
created
'
)
handleClose
()
}
catch
(
error
:
any
)
{
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
&&
needsMixedChannelCheck
(
form
.
platform
))
{
openMixedChannelDialog
({
message
:
error
.
response
?.
data
?.
message
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
submitCreateAccount
(
payload
)
}
}
)
return
}
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
}
finally
{
submitting
.
value
=
false
}
}
// Methods
const
resetForm
=
()
=>
{
step
.
value
=
1
...
...
@@ -2855,9 +2944,13 @@ const resetForm = () => {
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
antigravityMixedChannelConfirmed
.
value
=
false
clearMixedChannelDialog
()
}
const
handleClose
=
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
false
clearMixedChannelDialog
()
emit
(
'
close
'
)
}
...
...
@@ -2916,56 +3009,34 @@ const buildSoraExtra = (
}
// Helper function to create account with mixed channel warning handling
const
doCreateAccount
=
async
(
payload
:
any
)
=>
{
submitting
.
value
=
true
try
{
await
adminAPI
.
accounts
.
create
(
payload
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
emit
(
'
created
'
)
handleClose
()
}
catch
(
error
:
any
)
{
// Handle 409 mixed_channel_warning - show confirmation dialog
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
)
{
const
details
=
error
.
response
.
data
.
details
||
{
}
mixedChannelWarningDetails
.
value
=
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
pendingCreatePayload
.
value
=
payload
showMixedChannelWarning
.
value
=
true
}
else
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
}
}
finally
{
submitting
.
value
=
false
const
doCreateAccount
=
async
(
payload
:
CreateAccountRequest
)
=>
{
const
canContinue
=
await
ensureAntigravityMixedChannelConfirmed
(
async
()
=>
{
await
submitCreateAccount
(
payload
)
}
)
if
(
!
canContinue
)
{
return
}
await
submitCreateAccount
(
payload
)
}
// Handle mixed channel warning confirmation
const
handleMixedChannelConfirm
=
async
()
=>
{
showMixedChannelWarning
.
value
=
false
if
(
pendingCreatePayload
.
value
)
{
pendingCreatePayload
.
value
.
confirm_mixed_channel_risk
=
true
submitting
.
value
=
true
try
{
await
adminAPI
.
accounts
.
create
(
pendingCreatePayload
.
value
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
emit
(
'
created
'
)
handleClose
()
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
}
finally
{
submitting
.
value
=
false
pendingCreatePayload
.
value
=
null
}
const
action
=
mixedChannelWarningAction
.
value
if
(
!
action
)
{
clearMixedChannelDialog
()
return
}
clearMixedChannelDialog
()
submitting
.
value
=
true
try
{
await
action
()
}
finally
{
submitting
.
value
=
false
}
}
const
handleMixedChannelCancel
=
()
=>
{
showMixedChannelWarning
.
value
=
false
pendingCreatePayload
.
value
=
null
mixedChannelWarningDetails
.
value
=
null
clearMixedChannelDialog
()
}
const
handleSubmit
=
async
()
=>
{
...
...
@@ -2975,6 +3046,12 @@ const handleSubmit = async () => {
appStore
.
showError
(
t
(
'
admin.accounts.pleaseEnterAccountName
'
))
return
}
const
canContinue
=
await
ensureAntigravityMixedChannelConfirmed
(
async
()
=>
{
step
.
value
=
2
}
)
if
(
!
canContinue
)
{
return
}
step
.
value
=
2
return
}
...
...
@@ -3010,15 +3087,10 @@ const handleSubmit = async () => {
credentials
.
model_mapping
=
antigravityModelMapping
}
submitting
.
value
=
true
try
{
const
extra
=
mixedScheduling
.
value
?
{
mixed_scheduling
:
true
}
:
undefined
await
createAccountAndFinish
(
form
.
platform
,
'
apikey
'
,
credentials
,
extra
)
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
}
finally
{
submitting
.
value
=
false
}
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
const
extra
=
mixedScheduling
.
value
?
{
mixed_scheduling
:
true
}
:
undefined
await
createAccountAndFinish
(
form
.
platform
,
'
apikey
'
,
credentials
,
extra
)
return
}
...
...
@@ -3059,10 +3131,7 @@ const handleSubmit = async () => {
credentials
.
custom_error_codes
=
[...
selectedErrorCodes
.
value
]
}
// Add intercept warmup requests setting
if
(
interceptWarmupRequests
.
value
)
{
credentials
.
intercept_warmup_requests
=
true
}
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
if
(
!
applyTempUnschedConfig
(
credentials
))
{
return
}
...
...
@@ -3132,7 +3201,7 @@ const createAccountAndFinish = async (
if
(
!
applyTempUnschedConfig
(
credentials
))
{
return
}
await
adminAPI
.
accounts
.
create
({
await
doCreateAccount
({
name
:
form
.
name
,
notes
:
form
.
notes
,
platform
,
...
...
@@ -3147,9 +3216,6 @@ const createAccountAndFinish = async (
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
emit
(
'
created
'
)
handleClose
()
}
// OpenAI OAuth 授权码兑换
...
...
@@ -3497,7 +3563,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
// Note: Antigravity doesn't have buildExtraInfo, so we pass empty extra or rely on credentials
await
adminAPI
.
accounts
.
create
({
const
createPayload
=
withAntigravityConfirmFlag
({
name
:
accountName
,
notes
:
form
.
notes
,
platform
:
'
antigravity
'
,
...
...
@@ -3512,6 +3578,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
await
adminAPI
.
accounts
.
create
(
createPayload
)
successCount
++
}
catch
(
error
:
any
)
{
failedCount
++
...
...
@@ -3606,6 +3673,7 @@ const handleAntigravityExchange = async (authCode: string) => {
if
(
!
tokenInfo
)
return
const
credentials
=
antigravityOAuth
.
buildCredentials
(
tokenInfo
)
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
// Antigravity 只使用映射模式
const
antigravityModelMapping
=
buildModelMappingObject
(
'
mapping
'
,
...
...
@@ -3677,10 +3745,8 @@ const handleAnthropicExchange = async (authCode: string) => {
extra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
}
const
credentials
=
{
...
tokenInfo
,
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
}
const
credentials
:
Record
<
string
,
unknown
>
=
{
...
tokenInfo
}
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
await
createAccountAndFinish
(
form
.
platform
,
addMethod
.
value
as
AccountType
,
credentials
,
extra
)
}
catch
(
error
:
any
)
{
oauth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
...
...
@@ -3779,11 +3845,8 @@ const handleCookieAuth = async (sessionKey: string) => {
const
accountName
=
keys
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
// Merge interceptWarmupRequests into credentials
const
credentials
:
Record
<
string
,
unknown
>
=
{
...
tokenInfo
,
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
}
const
credentials
:
Record
<
string
,
unknown
>
=
{
...
tokenInfo
}
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
if
(
tempUnschedEnabled
.
value
)
{
credentials
.
temp_unschedulable_enabled
=
true
credentials
.
temp_unschedulable_rules
=
tempUnschedPayload
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
c7e18bd5
...
...
@@ -65,8 +65,8 @@
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.leaveEmptyToKeep
'
)
}}
</p>
</div>
<!-- Model Restriction Section (不适用于
Gemini 和
Antigravity) -->
<div
v-if=
"account.platform !==
'gemini' && account.platform !==
'antigravity'"
class=
"border-t border-gray-200 pt-4 dark:border-dark-600"
>
<!-- Model Restriction Section (不适用于 Antigravity) -->
<div
v-if=
"account.platform !== 'antigravity'"
class=
"border-t border-gray-200 pt-4 dark:border-dark-600"
>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.modelRestriction
'
)
}}
</label>
<div
...
...
@@ -349,34 +349,6 @@
<
/div
>
<
/div
>
<!--
Gemini
模型说明
-->
<
div
v
-
if
=
"
account.platform === 'gemini'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20
"
>
<
div
class
=
"
flex items-start gap-3
"
>
<
svg
class
=
"
h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
stroke
-
width
=
"
2
"
d
=
"
M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z
"
/>
<
/svg
>
<
div
>
<
p
class
=
"
text-sm font-medium text-blue-800 dark:text-blue-300
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthrough
'
)
}}
<
/p
>
<
p
class
=
"
mt-1 text-xs text-blue-700 dark:text-blue-400
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthroughDesc
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Upstream
fields
(
only
for
upstream
type
)
-->
...
...
@@ -641,9 +613,9 @@
<
/div
>
<
/div
>
<!--
Intercept
Warmup
Requests
(
Anthropic
onl
y
)
-->
<!--
Intercept
Warmup
Requests
(
Anthropic
/
Antigravit
y
)
-->
<
div
v
-
if
=
"
account?.platform === 'anthropic'
"
v
-
if
=
"
account?.platform === 'anthropic'
|| account?.platform === 'antigravity'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
...
...
@@ -1139,7 +1111,7 @@
<
ConfirmDialog
:
show
=
"
showMixedChannelWarning
"
:
title
=
"
t('admin.accounts.mixedChannelWarningTitle')
"
:
message
=
"
mixedChannelWarning
Details ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''
"
:
message
=
"
mixedChannelWarning
MessageText
"
:
confirm
-
text
=
"
t('common.confirm')
"
:
cancel
-
text
=
"
t('common.cancel')
"
:
danger
=
"
true
"
...
...
@@ -1154,7 +1126,7 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Account
,
Proxy
,
AdminGroup
}
from
'
@/types
'
import
type
{
Account
,
Proxy
,
AdminGroup
,
CheckMixedChannelResponse
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
...
@@ -1162,6 +1134,7 @@ import Icon from '@/components/icons/Icon.vue'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
ModelWhitelistSelector
from
'
@/components/account/ModelWhitelistSelector.vue
'
import
{
applyInterceptWarmup
}
from
'
@/components/account/credentialsBuilder
'
import
{
formatDateTimeLocalInput
,
parseDateTimeLocalInput
}
from
'
@/utils/format
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
...
...
@@ -1233,10 +1206,13 @@ const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-mod
const
getAntigravityModelMappingKey
=
createStableObjectKeyResolver
<
ModelMapping
>
(
'
edit-antigravity-model-mapping
'
)
const
getTempUnschedRuleKey
=
createStableObjectKeyResolver
<
TempUnschedRuleForm
>
(
'
edit-temp-unsched-rule
'
)
// Mixed channel warning dialog state
const
showMixedChannelWarning
=
ref
(
false
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
null
)
const
pendingUpdatePayload
=
ref
<
Record
<
string
,
unknown
>
|
null
>
(
null
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
null
)
const
mixedChannelWarningRawMessage
=
ref
(
''
)
const
mixedChannelWarningAction
=
ref
<
(()
=>
Promise
<
void
>
)
|
null
>
(
null
)
const
antigravityMixedChannelConfirmed
=
ref
(
false
)
// Quota control state (Anthropic OAuth/SetupToken only)
const
windowCostEnabled
=
ref
(
false
)
...
...
@@ -1297,6 +1273,13 @@ const defaultBaseUrl = computed(() => {
return
'
https://api.anthropic.com
'
}
)
const
mixedChannelWarningMessageText
=
computed
(()
=>
{
if
(
mixedChannelWarningDetails
.
value
)
{
return
t
(
'
admin.accounts.mixedChannelWarning
'
,
mixedChannelWarningDetails
.
value
)
}
return
mixedChannelWarningRawMessage
.
value
}
)
const
form
=
reactive
({
name
:
''
,
notes
:
''
,
...
...
@@ -1326,6 +1309,11 @@ watch(
()
=>
props
.
account
,
(
newAccount
)
=>
{
if
(
newAccount
)
{
antigravityMixedChannelConfirmed
.
value
=
false
showMixedChannelWarning
.
value
=
false
mixedChannelWarningDetails
.
value
=
null
mixedChannelWarningRawMessage
.
value
=
''
mixedChannelWarningAction
.
value
=
null
form
.
name
=
newAccount
.
name
form
.
notes
=
newAccount
.
notes
||
''
form
.
proxy_id
=
newAccount
.
proxy_id
...
...
@@ -1725,18 +1713,123 @@ function toPositiveNumber(value: unknown) {
return
Math
.
trunc
(
num
)
}
const
needsMixedChannelCheck
=
()
=>
props
.
account
?.
platform
===
'
antigravity
'
||
props
.
account
?.
platform
===
'
anthropic
'
const
buildMixedChannelDetails
=
(
resp
?:
CheckMixedChannelResponse
)
=>
{
const
details
=
resp
?.
details
if
(
!
details
)
{
return
null
}
return
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
}
const
clearMixedChannelDialog
=
()
=>
{
showMixedChannelWarning
.
value
=
false
mixedChannelWarningDetails
.
value
=
null
mixedChannelWarningRawMessage
.
value
=
''
mixedChannelWarningAction
.
value
=
null
}
const
openMixedChannelDialog
=
(
opts
:
{
response
?:
CheckMixedChannelResponse
message
?:
string
onConfirm
:
()
=>
Promise
<
void
>
}
)
=>
{
mixedChannelWarningDetails
.
value
=
buildMixedChannelDetails
(
opts
.
response
)
mixedChannelWarningRawMessage
.
value
=
opts
.
message
||
opts
.
response
?.
message
||
t
(
'
admin.accounts.failedToUpdate
'
)
mixedChannelWarningAction
.
value
=
opts
.
onConfirm
showMixedChannelWarning
.
value
=
true
}
const
withAntigravityConfirmFlag
=
(
payload
:
Record
<
string
,
unknown
>
)
=>
{
if
(
needsMixedChannelCheck
()
&&
antigravityMixedChannelConfirmed
.
value
)
{
return
{
...
payload
,
confirm_mixed_channel_risk
:
true
}
}
const
cloned
=
{
...
payload
}
delete
cloned
.
confirm_mixed_channel_risk
return
cloned
}
const
ensureAntigravityMixedChannelConfirmed
=
async
(
onConfirm
:
()
=>
Promise
<
void
>
):
Promise
<
boolean
>
=>
{
if
(
!
needsMixedChannelCheck
())
{
return
true
}
if
(
antigravityMixedChannelConfirmed
.
value
)
{
return
true
}
if
(
!
props
.
account
)
{
return
false
}
try
{
const
result
=
await
adminAPI
.
accounts
.
checkMixedChannelRisk
({
platform
:
props
.
account
.
platform
,
group_ids
:
form
.
group_ids
,
account_id
:
props
.
account
.
id
}
)
if
(
!
result
.
has_risk
)
{
return
true
}
openMixedChannelDialog
({
response
:
result
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
onConfirm
()
}
}
)
return
false
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
return
false
}
}
const
formatDateTimeLocal
=
formatDateTimeLocalInput
const
parseDateTimeLocal
=
parseDateTimeLocalInput
// Methods
const
handleClose
=
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
false
clearMixedChannelDialog
()
emit
(
'
close
'
)
}
const
submitUpdateAccount
=
async
(
accountID
:
number
,
updatePayload
:
Record
<
string
,
unknown
>
)
=>
{
submitting
.
value
=
true
try
{
const
updatedAccount
=
await
adminAPI
.
accounts
.
update
(
accountID
,
withAntigravityConfirmFlag
(
updatePayload
))
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountUpdated
'
))
emit
(
'
updated
'
,
updatedAccount
)
handleClose
()
}
catch
(
error
:
any
)
{
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
&&
needsMixedChannelCheck
())
{
openMixedChannelDialog
({
message
:
error
.
response
?.
data
?.
message
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
submitUpdateAccount
(
accountID
,
updatePayload
)
}
}
)
return
}
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
}
finally
{
submitting
.
value
=
false
}
}
const
handleSubmit
=
async
()
=>
{
if
(
!
props
.
account
)
return
const
accountID
=
props
.
account
.
id
submitting
.
value
=
true
const
updatePayload
:
Record
<
string
,
unknown
>
=
{
...
form
}
try
{
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
...
...
@@ -1768,7 +1861,6 @@ const handleSubmit = async () => {
newCredentials
.
api_key
=
currentCredentials
.
api_key
}
else
{
appStore
.
showError
(
t
(
'
admin.accounts.apiKeyIsRequired
'
))
submitting
.
value
=
false
return
}
...
...
@@ -1789,11 +1881,8 @@ const handleSubmit = async () => {
}
// Add intercept warmup requests setting
if
(
interceptWarmupRequests
.
value
)
{
newCredentials
.
intercept_warmup_requests
=
true
}
applyInterceptWarmup
(
newCredentials
,
interceptWarmupRequests
.
value
,
'
edit
'
)
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
submitting
.
value
=
false
return
}
...
...
@@ -1808,8 +1897,10 @@ const handleSubmit = async () => {
newCredentials
.
api_key
=
editApiKey
.
value
.
trim
()
}
// Add intercept warmup requests setting
applyInterceptWarmup
(
newCredentials
,
interceptWarmupRequests
.
value
,
'
edit
'
)
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
submitting
.
value
=
false
return
}
...
...
@@ -1819,13 +1910,8 @@ const handleSubmit = async () => {
const
currentCredentials
=
(
props
.
account
.
credentials
as
Record
<
string
,
unknown
>
)
||
{
}
const
newCredentials
:
Record
<
string
,
unknown
>
=
{
...
currentCredentials
}
if
(
interceptWarmupRequests
.
value
)
{
newCredentials
.
intercept_warmup_requests
=
true
}
else
{
delete
newCredentials
.
intercept_warmup_requests
}
applyInterceptWarmup
(
newCredentials
,
interceptWarmupRequests
.
value
,
'
edit
'
)
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
submitting
.
value
=
false
return
}
...
...
@@ -1955,52 +2041,36 @@ const handleSubmit = async () => {
updatePayload
.
extra
=
newExtra
}
const
updatedAccount
=
await
adminAPI
.
accounts
.
update
(
props
.
account
.
id
,
updatePayload
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountUpdated
'
))
emit
(
'
updated
'
,
updatedAccount
)
handleClose
()
}
catch
(
error
:
any
)
{
// Handle 409 mixed_channel_warning - show confirmation dialog
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
)
{
const
details
=
error
.
response
.
data
.
details
||
{
}
mixedChannelWarningDetails
.
value
=
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
pendingUpdatePayload
.
value
=
updatePayload
showMixedChannelWarning
.
value
=
true
}
else
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
const
canContinue
=
await
ensureAntigravityMixedChannelConfirmed
(
async
()
=>
{
await
submitUpdateAccount
(
accountID
,
updatePayload
)
}
)
if
(
!
canContinue
)
{
return
}
}
finally
{
submitting
.
value
=
false
await
submitUpdateAccount
(
accountID
,
updatePayload
)
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
}
}
// Handle mixed channel warning confirmation
const
handleMixedChannelConfirm
=
async
()
=>
{
showMixedChannelWarning
.
value
=
false
if
(
pendingUpdatePayload
.
value
&&
props
.
account
)
{
pendingUpdatePayload
.
value
.
confirm_mixed_channel_risk
=
true
submitting
.
value
=
true
try
{
const
updatedAccount
=
await
adminAPI
.
accounts
.
update
(
props
.
account
.
id
,
pendingUpdatePayload
.
value
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountUpdated
'
))
emit
(
'
updated
'
,
updatedAccount
)
handleClose
()
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
}
finally
{
submitting
.
value
=
false
pendingUpdatePayload
.
value
=
null
}
const
action
=
mixedChannelWarningAction
.
value
if
(
!
action
)
{
clearMixedChannelDialog
()
return
}
clearMixedChannelDialog
()
submitting
.
value
=
true
try
{
await
action
()
}
finally
{
submitting
.
value
=
false
}
}
const
handleMixedChannelCancel
=
()
=>
{
showMixedChannelWarning
.
value
=
false
pendingUpdatePayload
.
value
=
null
mixedChannelWarningDetails
.
value
=
null
clearMixedChannelDialog
()
}
<
/script
>
frontend/src/components/account/__tests__/credentialsBuilder.spec.ts
0 → 100644
View file @
c7e18bd5
import
{
describe
,
it
,
expect
}
from
'
vitest
'
import
{
applyInterceptWarmup
}
from
'
../credentialsBuilder
'
describe
(
'
applyInterceptWarmup
'
,
()
=>
{
it
(
'
create + enabled=true: should set intercept_warmup_requests to true
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
access_token
:
'
tok
'
}
applyInterceptWarmup
(
creds
,
true
,
'
create
'
)
expect
(
creds
.
intercept_warmup_requests
).
toBe
(
true
)
})
it
(
'
create + enabled=false: should not add the field
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
access_token
:
'
tok
'
}
applyInterceptWarmup
(
creds
,
false
,
'
create
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
it
(
'
edit + enabled=true: should set intercept_warmup_requests to true
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
}
applyInterceptWarmup
(
creds
,
true
,
'
edit
'
)
expect
(
creds
.
intercept_warmup_requests
).
toBe
(
true
)
})
it
(
'
edit + enabled=false + field exists: should delete the field
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
,
intercept_warmup_requests
:
true
}
applyInterceptWarmup
(
creds
,
false
,
'
edit
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
it
(
'
edit + enabled=false + field absent: should not throw
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
}
applyInterceptWarmup
(
creds
,
false
,
'
edit
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
it
(
'
should not affect other fields
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
,
base_url
:
'
url
'
,
intercept_warmup_requests
:
true
}
applyInterceptWarmup
(
creds
,
false
,
'
edit
'
)
expect
(
creds
.
api_key
).
toBe
(
'
sk
'
)
expect
(
creds
.
base_url
).
toBe
(
'
url
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
})
frontend/src/components/account/credentialsBuilder.ts
0 → 100644
View file @
c7e18bd5
export
function
applyInterceptWarmup
(
credentials
:
Record
<
string
,
unknown
>
,
enabled
:
boolean
,
mode
:
'
create
'
|
'
edit
'
):
void
{
if
(
enabled
)
{
credentials
.
intercept_warmup_requests
=
true
}
else
if
(
mode
===
'
edit
'
)
{
delete
credentials
.
intercept_warmup_requests
}
}
frontend/src/composables/useModelWhitelist.ts
View file @
c7e18bd5
...
...
@@ -76,6 +76,7 @@ const antigravityModels = [
// Claude 4.5+ 系列
'
claude-opus-4-6
'
,
'
claude-opus-4-5-thinking
'
,
'
claude-sonnet-4-6
'
,
'
claude-sonnet-4-5
'
,
'
claude-sonnet-4-5-thinking
'
,
// Gemini 2.5 系列
...
...
@@ -88,6 +89,9 @@ const antigravityModels = [
'
gemini-3-pro-high
'
,
'
gemini-3-pro-low
'
,
'
gemini-3-pro-image
'
,
// Gemini 3.1 系列
'
gemini-3.1-pro-high
'
,
'
gemini-3.1-pro-low
'
,
// 其他
'
gpt-oss-120b-medium
'
,
'
tab_flash_lite_preview
'
...
...
@@ -301,6 +305,7 @@ const antigravityPresetMappings = [
{
label
:
'
3-Flash透传
'
,
from
:
'
gemini-3-flash
'
,
to
:
'
gemini-3-flash
'
,
color
:
'
bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400
'
},
{
label
:
'
2.5-Flash-Lite透传
'
,
from
:
'
gemini-2.5-flash-lite
'
,
to
:
'
gemini-2.5-flash-lite
'
,
color
:
'
bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400
'
},
// 精确映射
{
label
:
'
Sonnet 4.6
'
,
from
:
'
claude-sonnet-4-6
'
,
to
:
'
claude-sonnet-4-6
'
,
color
:
'
bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400
'
},
{
label
:
'
Sonnet 4.5
'
,
from
:
'
claude-sonnet-4-5
'
,
to
:
'
claude-sonnet-4-5
'
,
color
:
'
bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400
'
},
{
label
:
'
Opus 4.6-thinking
'
,
from
:
'
claude-opus-4-6-thinking
'
,
to
:
'
claude-opus-4-6-thinking
'
,
color
:
'
bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400
'
}
]
...
...
frontend/src/i18n/locales/en.ts
View file @
c7e18bd5
...
...
@@ -2047,7 +2047,7 @@ export default {
gemini3Pro
:
'
G3P
'
,
gemini3Flash
:
'
G3F
'
,
gemini3Image
:
'
G3I
'
,
claude
45
:
'
C
4.5
'
claude
:
'
C
laude
'
},
tier
:
{
free
:
'
Free
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
c7e18bd5
...
...
@@ -1583,7 +1583,7 @@ export default {
gemini3Pro
:
'
G3P
'
,
gemini3Flash
:
'
G3F
'
,
gemini3Image
:
'
G3I
'
,
claude
45
:
'
C
4.5
'
claude
:
'
C
laude
'
},
tier
:
{
free
:
'
Free
'
,
...
...
frontend/src/types/index.ts
View file @
c7e18bd5
...
...
@@ -581,6 +581,7 @@ export interface GeminiCredentials {
token_type
?:
string
scope
?:
string
expires_at
?:
string
model_mapping
?:
Record
<
string
,
string
>
}
export
interface
TempUnschedulableRule
{
...
...
@@ -766,6 +767,26 @@ export interface UpdateAccountRequest {
confirm_mixed_channel_risk
?:
boolean
}
export
interface
CheckMixedChannelRequest
{
platform
:
AccountPlatform
group_ids
:
number
[]
account_id
?:
number
}
export
interface
MixedChannelWarningDetails
{
group_id
:
number
group_name
:
string
current_platform
:
string
other_platform
:
string
}
export
interface
CheckMixedChannelResponse
{
has_risk
:
boolean
error
?:
string
message
?:
string
details
?:
MixedChannelWarningDetails
}
export
interface
CreateProxyRequest
{
name
:
string
protocol
:
ProxyProtocol
...
...
Prev
1
2
3
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment