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
b6527523
Commit
b6527523
authored
Mar 30, 2026
by
shaw
Browse files
feat: Anthropic oauth/setup-token账号支持自定义转发URL
parent
fdd8499f
Changes
9
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/dto/mappers.go
View file @
b6527523
...
...
@@ -268,6 +268,14 @@ func AccountFromServiceShallow(a *service.Account) *Account {
target
:=
a
.
GetCacheTTLOverrideTarget
()
out
.
CacheTTLOverrideTarget
=
&
target
}
// 自定义 Base URL 中继转发
if
a
.
IsCustomBaseURLEnabled
()
{
enabled
:=
true
out
.
CustomBaseURLEnabled
=
&
enabled
if
customURL
:=
a
.
GetCustomBaseURL
();
customURL
!=
""
{
out
.
CustomBaseURL
=
&
customURL
}
}
}
// 提取账号配额限制(apikey / bedrock 类型有效)
...
...
backend/internal/handler/dto/types.go
View file @
b6527523
...
...
@@ -198,6 +198,10 @@ type Account struct {
CacheTTLOverrideEnabled
*
bool
`json:"cache_ttl_override_enabled,omitempty"`
CacheTTLOverrideTarget
*
string
`json:"cache_ttl_override_target,omitempty"`
// 自定义 Base URL 中继转发(仅 Anthropic OAuth/SetupToken 账号有效)
CustomBaseURLEnabled
*
bool
`json:"custom_base_url_enabled,omitempty"`
CustomBaseURL
*
string
`json:"custom_base_url,omitempty"`
// API Key 账号配额限制
QuotaLimit
*
float64
`json:"quota_limit,omitempty"`
QuotaUsed
*
float64
`json:"quota_used,omitempty"`
...
...
backend/internal/service/account.go
View file @
b6527523
...
...
@@ -1229,6 +1229,28 @@ func (a *Account) IsSessionIDMaskingEnabled() bool {
return
false
}
// IsCustomBaseURLEnabled 检查是否启用自定义 base URL 中继转发
// 仅适用于 Anthropic OAuth/SetupToken 类型账号
func
(
a
*
Account
)
IsCustomBaseURLEnabled
()
bool
{
if
!
a
.
IsAnthropicOAuthOrSetupToken
()
{
return
false
}
if
a
.
Extra
==
nil
{
return
false
}
if
v
,
ok
:=
a
.
Extra
[
"custom_base_url_enabled"
];
ok
{
if
enabled
,
ok
:=
v
.
(
bool
);
ok
{
return
enabled
}
}
return
false
}
// GetCustomBaseURL 返回自定义中继服务的 base URL
func
(
a
*
Account
)
GetCustomBaseURL
()
string
{
return
a
.
GetExtraString
(
"custom_base_url"
)
}
// IsCacheTTLOverrideEnabled 检查是否启用缓存 TTL 强制替换
// 仅适用于 Anthropic OAuth/SetupToken 类型账号
// 启用后将所有 cache creation tokens 归入指定的 TTL 类型(5m 或 1h)
...
...
backend/internal/service/gateway_service.go
View file @
b6527523
...
...
@@ -12,6 +12,7 @@ import (
"log/slog"
mathrand
"math/rand"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
...
...
@@ -4150,10 +4151,12 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
return
nil
,
err
}
// 获取代理URL
// 获取代理URL
(自定义 base URL 模式下,proxy 通过 buildCustomRelayURL 作为查询参数传递)
proxyURL
:=
""
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
=
account
.
Proxy
.
URL
()
if
!
account
.
IsCustomBaseURLEnabled
()
||
account
.
GetCustomBaseURL
()
==
""
{
proxyURL
=
account
.
Proxy
.
URL
()
}
}
// 解析 TLS 指纹 profile(同一请求生命周期内不变,避免重试循环中重复解析)
...
...
@@ -5628,6 +5631,16 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
}
targetURL
=
validatedURL
+
"/v1/messages?beta=true"
}
}
else
if
account
.
IsCustomBaseURLEnabled
()
{
customURL
:=
account
.
GetCustomBaseURL
()
if
customURL
==
""
{
return
nil
,
fmt
.
Errorf
(
"custom_base_url is enabled but not configured for account %d"
,
account
.
ID
)
}
validatedURL
,
err
:=
s
.
validateUpstreamBaseURL
(
customURL
)
if
err
!=
nil
{
return
nil
,
err
}
targetURL
=
s
.
buildCustomRelayURL
(
validatedURL
,
"/v1/messages"
,
account
)
}
clientHeaders
:=
http
.
Header
{}
...
...
@@ -8063,10 +8076,12 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context,
return
err
}
// 获取代理URL
// 获取代理URL
(自定义 base URL 模式下,proxy 通过 buildCustomRelayURL 作为查询参数传递)
proxyURL
:=
""
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
=
account
.
Proxy
.
URL
()
if
!
account
.
IsCustomBaseURLEnabled
()
||
account
.
GetCustomBaseURL
()
==
""
{
proxyURL
=
account
.
Proxy
.
URL
()
}
}
// 发送请求
...
...
@@ -8345,6 +8360,16 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con
}
targetURL
=
validatedURL
+
"/v1/messages/count_tokens?beta=true"
}
}
else
if
account
.
IsCustomBaseURLEnabled
()
{
customURL
:=
account
.
GetCustomBaseURL
()
if
customURL
==
""
{
return
nil
,
fmt
.
Errorf
(
"custom_base_url is enabled but not configured for account %d"
,
account
.
ID
)
}
validatedURL
,
err
:=
s
.
validateUpstreamBaseURL
(
customURL
)
if
err
!=
nil
{
return
nil
,
err
}
targetURL
=
s
.
buildCustomRelayURL
(
validatedURL
,
"/v1/messages/count_tokens"
,
account
)
}
clientHeaders
:=
http
.
Header
{}
...
...
@@ -8471,6 +8496,19 @@ func (s *GatewayService) countTokensError(c *gin.Context, status int, errType, m
})
}
// buildCustomRelayURL 构建自定义中继转发 URL
// 在 path 后附加 beta=true 和可选的 proxy 查询参数
func
(
s
*
GatewayService
)
buildCustomRelayURL
(
baseURL
,
path
string
,
account
*
Account
)
string
{
u
:=
strings
.
TrimRight
(
baseURL
,
"/"
)
+
path
+
"?beta=true"
if
account
.
ProxyID
!=
nil
&&
account
.
Proxy
!=
nil
{
proxyURL
:=
account
.
Proxy
.
URL
()
if
proxyURL
!=
""
{
u
+=
"&proxy="
+
url
.
QueryEscape
(
proxyURL
)
}
}
return
u
}
func
(
s
*
GatewayService
)
validateUpstreamBaseURL
(
raw
string
)
(
string
,
error
)
{
if
s
.
cfg
!=
nil
&&
!
s
.
cfg
.
Security
.
URLAllowlist
.
Enabled
{
normalized
,
err
:=
urlvalidator
.
ValidateURLFormat
(
raw
,
s
.
cfg
.
Security
.
URLAllowlist
.
AllowInsecureHTTP
)
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
b6527523
...
...
@@ -2245,6 +2245,41 @@
<
/p
>
<
/div
>
<
/div
>
<!--
Custom
Base
URL
Relay
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.customBaseUrl.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.customBaseUrl.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
customBaseUrlEnabled = !customBaseUrlEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
customBaseUrlEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
customBaseUrlEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
div
v
-
if
=
"
customBaseUrlEnabled
"
class
=
"
mt-3
"
>
<
input
v
-
model
=
"
customBaseUrl
"
type
=
"
text
"
class
=
"
input
"
:
placeholder
=
"
t('admin.accounts.quotaControl.customBaseUrl.urlHint')
"
/>
<
/div
>
<
/div
>
<
/div
>
<
div
>
...
...
@@ -3095,6 +3130,8 @@ const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([])
const
sessionIdMaskingEnabled
=
ref
(
false
)
const
cacheTTLOverrideEnabled
=
ref
(
false
)
const
cacheTTLOverrideTarget
=
ref
<
string
>
(
'
5m
'
)
const
customBaseUrlEnabled
=
ref
(
false
)
const
customBaseUrl
=
ref
(
''
)
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
const
geminiTierGoogleOne
=
ref
<
'
google_one_free
'
|
'
google_ai_pro
'
|
'
google_ai_ultra
'
>
(
'
google_one_free
'
)
...
...
@@ -3765,6 +3802,8 @@ const resetForm = () => {
sessionIdMaskingEnabled
.
value
=
false
cacheTTLOverrideEnabled
.
value
=
false
cacheTTLOverrideTarget
.
value
=
'
5m
'
customBaseUrlEnabled
.
value
=
false
customBaseUrl
.
value
=
''
allowOverages
.
value
=
false
antigravityAccountType
.
value
=
'
oauth
'
upstreamBaseUrl
.
value
=
''
...
...
@@ -4856,6 +4895,12 @@ const handleAnthropicExchange = async (authCode: string) => {
extra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
}
// Add custom base URL settings
if
(
customBaseUrlEnabled
.
value
&&
customBaseUrl
.
value
.
trim
())
{
extra
.
custom_base_url_enabled
=
true
extra
.
custom_base_url
=
customBaseUrl
.
value
.
trim
()
}
const
credentials
:
Record
<
string
,
unknown
>
=
{
...
tokenInfo
}
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
await
createAccountAndFinish
(
form
.
platform
,
addMethod
.
value
as
AccountType
,
credentials
,
extra
)
...
...
@@ -4974,6 +5019,12 @@ const handleCookieAuth = async (sessionKey: string) => {
extra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
}
// Add custom base URL settings
if
(
customBaseUrlEnabled
.
value
&&
customBaseUrl
.
value
.
trim
())
{
extra
.
custom_base_url_enabled
=
true
extra
.
custom_base_url
=
customBaseUrl
.
value
.
trim
()
}
const
accountName
=
keys
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
const
credentials
:
Record
<
string
,
unknown
>
=
{
...
tokenInfo
}
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
b6527523
...
...
@@ -1580,6 +1580,41 @@
<
/p
>
<
/div
>
<
/div
>
<!--
Custom
Base
URL
Relay
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.customBaseUrl.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.customBaseUrl.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
customBaseUrlEnabled = !customBaseUrlEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
customBaseUrlEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
customBaseUrlEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
div
v
-
if
=
"
customBaseUrlEnabled
"
class
=
"
mt-3
"
>
<
input
v
-
model
=
"
customBaseUrl
"
type
=
"
text
"
class
=
"
input
"
:
placeholder
=
"
t('admin.accounts.quotaControl.customBaseUrl.urlHint')
"
/>
<
/div
>
<
/div
>
<
/div
>
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
...
...
@@ -1854,6 +1889,8 @@ const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([])
const
sessionIdMaskingEnabled
=
ref
(
false
)
const
cacheTTLOverrideEnabled
=
ref
(
false
)
const
cacheTTLOverrideTarget
=
ref
<
string
>
(
'
5m
'
)
const
customBaseUrlEnabled
=
ref
(
false
)
const
customBaseUrl
=
ref
(
''
)
// OpenAI 自动透传开关(OAuth/API Key)
const
openaiPassthroughEnabled
=
ref
(
false
)
...
...
@@ -2482,6 +2519,8 @@ function loadQuotaControlSettings(account: Account) {
sessionIdMaskingEnabled
.
value
=
false
cacheTTLOverrideEnabled
.
value
=
false
cacheTTLOverrideTarget
.
value
=
'
5m
'
customBaseUrlEnabled
.
value
=
false
customBaseUrl
.
value
=
''
// Only applies to Anthropic OAuth/SetupToken accounts
if
(
account
.
platform
!==
'
anthropic
'
||
(
account
.
type
!==
'
oauth
'
&&
account
.
type
!==
'
setup-token
'
))
{
...
...
@@ -2528,6 +2567,12 @@ function loadQuotaControlSettings(account: Account) {
cacheTTLOverrideEnabled
.
value
=
true
cacheTTLOverrideTarget
.
value
=
account
.
cache_ttl_override_target
||
'
5m
'
}
// Load custom base URL setting
if
(
account
.
custom_base_url_enabled
===
true
)
{
customBaseUrlEnabled
.
value
=
true
customBaseUrl
.
value
=
account
.
custom_base_url
||
''
}
}
function
formatTempUnschedKeywords
(
value
:
unknown
)
{
...
...
@@ -2980,6 +3025,15 @@ const handleSubmit = async () => {
delete
newExtra
.
cache_ttl_override_target
}
// Custom base URL relay setting
if
(
customBaseUrlEnabled
.
value
&&
customBaseUrl
.
value
.
trim
())
{
newExtra
.
custom_base_url_enabled
=
true
newExtra
.
custom_base_url
=
customBaseUrl
.
value
.
trim
()
}
else
{
delete
newExtra
.
custom_base_url_enabled
delete
newExtra
.
custom_base_url
}
updatePayload
.
extra
=
newExtra
}
...
...
frontend/src/i18n/locales/en.ts
View file @
b6527523
...
...
@@ -2318,6 +2318,11 @@ export default {
target
:
'
Target TTL
'
,
targetHint
:
'
Select the TTL tier for billing
'
},
customBaseUrl
:
{
label
:
'
Custom Relay URL
'
,
hint
:
'
Forward requests to a custom relay service. Proxy URL will be passed as a query parameter.
'
,
urlHint
:
'
Relay service URL (e.g., https://relay.example.com)
'
,
},
clientAffinity
:
{
label
:
'
Client Affinity Scheduling
'
,
hint
:
'
When enabled, new sessions prefer accounts previously used by this client to reduce account switching
'
...
...
frontend/src/i18n/locales/zh.ts
View file @
b6527523
...
...
@@ -2462,6 +2462,11 @@ export default {
target
:
'
目标 TTL
'
,
targetHint
:
'
选择计费使用的 TTL 类型
'
},
customBaseUrl
:
{
label
:
'
自定义转发地址
'
,
hint
:
'
启用后将请求转发到自定义中继服务,代理地址将作为 URL 参数传递给中继服务
'
,
urlHint
:
'
中继服务地址(如 https://relay.example.com)
'
,
},
clientAffinity
:
{
label
:
'
客户端亲和调度
'
,
hint
:
'
启用后,新会话会优先调度到该客户端之前使用过的账号,避免频繁切换账号
'
...
...
frontend/src/types/index.ts
View file @
b6527523
...
...
@@ -734,6 +734,10 @@ export interface Account {
cache_ttl_override_enabled
?:
boolean
|
null
cache_ttl_override_target
?:
string
|
null
// 自定义 Base URL 中继转发(仅 Anthropic OAuth/SetupToken 账号有效)
custom_base_url_enabled
?:
boolean
|
null
custom_base_url
?:
string
|
null
// 客户端亲和调度(仅 Anthropic/Antigravity 平台有效)
// 启用后新会话会优先调度到客户端之前使用过的账号
client_affinity_enabled
?:
boolean
|
null
...
...
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