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
1f2ced89
"frontend/src/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "4251a5a451ab440e37280f4d4e1f44b97fb9f0d8"
Commit
1f2ced89
authored
Jan 03, 2026
by
ianshaw
Browse files
merge: 合并 main 分支解决冲突
parents
112a2d08
631ba25e
Changes
7
Hide whitespace changes
Inline
Side-by-side
backend/internal/pkg/antigravity/request_transformer.go
View file @
1f2ced89
...
...
@@ -14,24 +14,15 @@ func TransformClaudeToGemini(claudeReq *ClaudeRequest, projectID, mappedModel st
// 用于存储 tool_use id -> name 映射
toolIDToName
:=
make
(
map
[
string
]
string
)
// 检测是否启用 thinking
isThinkingEnabled
:=
claudeReq
.
Thinking
!=
nil
&&
claudeReq
.
Thinking
.
Type
==
"enabled"
// 只有 Gemini 模型支持 dummy thought workaround
// Claude 模型通过 Vertex/Google API 需要有效的 thought signatures
allowDummyThought
:=
strings
.
HasPrefix
(
mappedModel
,
"gemini-"
)
// 检测是否启用 thinking
requestedThinkingEnabled
:=
claudeReq
.
Thinking
!=
nil
&&
claudeReq
.
Thinking
.
Type
==
"enabled"
// antigravity(v1internal) 下,Gemini 与 Claude 的 “thinking” 都可能涉及 thoughtSignature 链路:
// - Gemini:支持 dummy signature 跳过校验
// - Claude:需要透传上游签名(否则容易 400)
isThinkingEnabled
:=
requestedThinkingEnabled
thoughtSignatureMode
:=
thoughtSignatureModePreserve
if
allowDummyThought
{
thoughtSignatureMode
=
thoughtSignatureModeDummy
}
// 1. 构建 contents
contents
,
err
:=
buildContents
(
claudeReq
.
Messages
,
toolIDToName
,
isThinkingEnabled
,
thoughtSignatureMode
)
contents
,
err
:=
buildContents
(
claudeReq
.
Messages
,
toolIDToName
,
isThinkingEnabled
,
allowDummyThought
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"build contents: %w"
,
err
)
}
...
...
@@ -129,7 +120,7 @@ func buildSystemInstruction(system json.RawMessage, modelName string) *GeminiCon
}
// buildContents 构建 contents
func
buildContents
(
messages
[]
ClaudeMessage
,
toolIDToName
map
[
string
]
string
,
isThinkingEnabled
bool
,
thoughtSignatureMode
thoughtSignatureMode
)
([]
GeminiContent
,
error
)
{
func
buildContents
(
messages
[]
ClaudeMessage
,
toolIDToName
map
[
string
]
string
,
isThinkingEnabled
,
allowDummyThought
bool
)
([]
GeminiContent
,
error
)
{
var
contents
[]
GeminiContent
for
i
,
msg
:=
range
messages
{
...
...
@@ -138,30 +129,23 @@ func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isT
role
=
"model"
}
parts
,
err
:=
buildParts
(
msg
.
Content
,
toolIDToName
,
thoughtSignatureMode
)
parts
,
err
:=
buildParts
(
msg
.
Content
,
toolIDToName
,
allowDummyThought
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"build parts for message %d: %w"
,
i
,
err
)
}
allowDummyThought
:=
thoughtSignatureMode
==
thoughtSignatureModeDummy
// 只有 Gemini 模型支持 dummy thinking block workaround
// 只对最后一条 assistant 消息添加(Pre-fill 场景)
// 历史 assistant 消息不能添加没有 signature 的 dummy thinking block
if
allowDummyThought
&&
role
==
"model"
&&
isThinkingEnabled
&&
i
==
len
(
messages
)
-
1
{
hasThoughtPart
:=
false
firstPartIsThought
:=
false
for
idx
,
p
:=
range
parts
{
for
_
,
p
:=
range
parts
{
if
p
.
Thought
{
hasThoughtPart
=
true
if
idx
==
0
{
firstPartIsThought
=
true
}
break
}
}
// 如果没有thinking part,或者有thinking part但不在第一个位置,都需要在开头添加dummy thinking block
if
len
(
parts
)
>
0
&&
(
!
hasThoughtPart
||
!
firstPartIsThought
)
{
if
!
hasThoughtPart
&&
len
(
parts
)
>
0
{
// 在开头添加 dummy thinking block
parts
=
append
([]
GeminiPart
{{
Text
:
"Thinking..."
,
...
...
@@ -189,18 +173,8 @@ func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isT
const
dummyThoughtSignature
=
"skip_thought_signature_validator"
// buildParts 构建消息的 parts
type
thoughtSignatureMode
int
const
(
thoughtSignatureModePreserve
thoughtSignatureMode
=
iota
thoughtSignatureModeDummy
)
// buildParts 构建消息的 parts
// thoughtSignatureMode:
// - dummy: 用 dummy signature 跳过 Gemini thoughtSignature 校验
// - preserve: 透传输入中的 signature(主要用于 Claude via Vertex 的签名链路)
func
buildParts
(
content
json
.
RawMessage
,
toolIDToName
map
[
string
]
string
,
thoughtSignatureMode
thoughtSignatureMode
)
([]
GeminiPart
,
error
)
{
// allowDummyThought: 只有 Gemini 模型支持 dummy thought signature
func
buildParts
(
content
json
.
RawMessage
,
toolIDToName
map
[
string
]
string
,
allowDummyThought
bool
)
([]
GeminiPart
,
error
)
{
var
parts
[]
GeminiPart
// 尝试解析为字符串
...
...
@@ -226,40 +200,22 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, thought
}
case
"thinking"
:
signature
:=
strings
.
TrimSpace
(
block
.
Signature
)
if
thoughtSignatureMode
==
thoughtSignatureModeDummy
{
// Gemini 模型可以使用 dummy signature
parts
=
append
(
parts
,
GeminiPart
{
Text
:
block
.
Thinking
,
Thought
:
true
,
ThoughtSignature
:
dummyThoughtSignature
,
})
continue
part
:=
GeminiPart
{
Text
:
block
.
Thinking
,
Thought
:
true
,
}
// Claude via Vertex:
// - signature 是上游返回的完整性令牌;本地不需要/无法验证,只能透传
// - 缺失/无效 signature(例如来自 Gemini 的 dummy signature)会导致上游 400
//
- 为避免泄露 thinking 内容,缺失/无效
signature 的 thinking
直接丢弃
if
signature
==
""
||
signature
==
dummyThoughtSignature
{
// 保留原有 signature(Claude 模型需要有效的 signature)
if
block
.
Signature
!=
""
{
part
.
ThoughtSignature
=
block
.
Signature
}
else
if
!
allowDummyThought
{
//
Claude 模型需要有效 signature,跳过无
signature 的 thinking
block
log
.
Printf
(
"Warning: skipping thinking block without signature for Claude model"
)
continue
}
// 兼容:用 Claude 的 "thinking" 块承载两类东西
// 1) 真正的 thought 文本(thinking != "")-> Gemini thought part
// 2) 仅承载 signature 的空 thinking 块(thinking == "")-> Gemini signature-only part
if
strings
.
TrimSpace
(
block
.
Thinking
)
==
""
{
parts
=
append
(
parts
,
GeminiPart
{
ThoughtSignature
:
signature
,
})
}
else
{
parts
=
append
(
parts
,
GeminiPart
{
Text
:
block
.
Thinking
,
Thought
:
true
,
ThoughtSignature
:
signature
,
})
// Gemini 模型使用 dummy signature
part
.
ThoughtSignature
=
dummyThoughtSignature
}
parts
=
append
(
parts
,
part
)
case
"image"
:
if
block
.
Source
!=
nil
&&
block
.
Source
.
Type
==
"base64"
{
...
...
@@ -284,15 +240,10 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, thought
ID
:
block
.
ID
,
},
}
switch
thoughtSignatureMode
{
case
thoughtSignatureModeDummy
:
// 只有 Gemini 模型使用 dummy signature
// Claude 模型不设置 signature(避免验证问题)
if
allowDummyThought
{
part
.
ThoughtSignature
=
dummyThoughtSignature
case
thoughtSignatureModePreserve
:
// Claude via Vertex:透传 tool_use 的 signature(如果有)
// 注意:跨模型混用时可能出现 dummy signature,这里直接丢弃以避免 400。
if
sig
:=
strings
.
TrimSpace
(
block
.
Signature
);
sig
!=
""
&&
sig
!=
dummyThoughtSignature
{
part
.
ThoughtSignature
=
sig
}
}
parts
=
append
(
parts
,
part
)
...
...
@@ -631,9 +582,11 @@ func cleanSchemaValue(value any) any {
continue
}
// 递归清理所有值
result
[
k
]
=
cleanSchemaValue
(
val
)
}
return
result
case
[]
any
:
// 递归处理数组中的每个元素
cleaned
:=
make
([]
any
,
0
,
len
(
v
))
...
...
backend/internal/pkg/antigravity/response_transformer.go
View file @
1f2ced89
...
...
@@ -237,11 +237,7 @@ func (p *NonStreamingProcessor) buildResponse(geminiResp *GeminiResponse, respon
usage
:=
ClaudeUsage
{}
if
geminiResp
.
UsageMetadata
!=
nil
{
cached
:=
geminiResp
.
UsageMetadata
.
CachedContentTokenCount
prompt
:=
geminiResp
.
UsageMetadata
.
PromptTokenCount
if
cached
>
prompt
{
cached
=
prompt
}
usage
.
InputTokens
=
prompt
-
cached
usage
.
InputTokens
=
geminiResp
.
UsageMetadata
.
PromptTokenCount
-
cached
usage
.
OutputTokens
=
geminiResp
.
UsageMetadata
.
CandidatesTokenCount
usage
.
CacheReadInputTokens
=
cached
}
...
...
backend/internal/pkg/antigravity/stream_transformer.go
View file @
1f2ced89
...
...
@@ -81,11 +81,7 @@ func (p *StreamingProcessor) ProcessLine(line string) []byte {
// 但 Claude 的 input_tokens 不包含 cache_read_input_tokens,需要减去
if
geminiResp
.
UsageMetadata
!=
nil
{
cached
:=
geminiResp
.
UsageMetadata
.
CachedContentTokenCount
prompt
:=
geminiResp
.
UsageMetadata
.
PromptTokenCount
if
cached
>
prompt
{
cached
=
prompt
}
p
.
inputTokens
=
prompt
-
cached
p
.
inputTokens
=
geminiResp
.
UsageMetadata
.
PromptTokenCount
-
cached
p
.
outputTokens
=
geminiResp
.
UsageMetadata
.
CandidatesTokenCount
p
.
cacheReadTokens
=
cached
}
...
...
@@ -134,11 +130,7 @@ func (p *StreamingProcessor) emitMessageStart(v1Resp *V1InternalResponse) []byte
usage
:=
ClaudeUsage
{}
if
v1Resp
.
Response
.
UsageMetadata
!=
nil
{
cached
:=
v1Resp
.
Response
.
UsageMetadata
.
CachedContentTokenCount
prompt
:=
v1Resp
.
Response
.
UsageMetadata
.
PromptTokenCount
if
cached
>
prompt
{
cached
=
prompt
}
usage
.
InputTokens
=
prompt
-
cached
usage
.
InputTokens
=
v1Resp
.
Response
.
UsageMetadata
.
PromptTokenCount
-
cached
usage
.
OutputTokens
=
v1Resp
.
Response
.
UsageMetadata
.
CandidatesTokenCount
usage
.
CacheReadInputTokens
=
cached
}
...
...
backend/internal/service/account_usage_service.go
View file @
1f2ced89
...
...
@@ -4,11 +4,9 @@ import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
)
...
...
@@ -84,18 +82,14 @@ const (
// UsageCache 封装账户使用量相关的缓存
type
UsageCache
struct
{
apiCache
*
sync
.
Map
// accountID -> *apiUsageCache
windowStatsCache
*
sync
.
Map
// accountID -> *windowStatsCache
antigravityCache
*
sync
.
Map
// accountID -> *antigravityUsageCache
apiCache
sync
.
Map
// accountID -> *apiUsageCache
windowStatsCache
sync
.
Map
// accountID -> *windowStatsCache
antigravityCache
sync
.
Map
// accountID -> *antigravityUsageCache
}
// NewUsageCache 创建 UsageCache 实例
func
NewUsageCache
()
*
UsageCache
{
return
&
UsageCache
{
apiCache
:
&
sync
.
Map
{},
antigravityCache
:
&
sync
.
Map
{},
windowStatsCache
:
&
sync
.
Map
{},
}
return
&
UsageCache
{}
}
// WindowStats 窗口期统计
...
...
@@ -159,7 +153,7 @@ type AccountUsageService struct {
usageLogRepo
UsageLogRepository
usageFetcher
ClaudeUsageFetcher
geminiQuotaService
*
GeminiQuotaService
antigravityQuotaFetcher
QuotaFetcher
antigravityQuotaFetcher
*
Antigravity
QuotaFetcher
cache
*
UsageCache
}
...
...
@@ -172,33 +166,12 @@ func NewAccountUsageService(
antigravityQuotaFetcher
*
AntigravityQuotaFetcher
,
cache
*
UsageCache
,
)
*
AccountUsageService
{
if
cache
==
nil
{
cache
=
&
UsageCache
{
apiCache
:
&
sync
.
Map
{},
antigravityCache
:
&
sync
.
Map
{},
windowStatsCache
:
&
sync
.
Map
{},
}
}
if
cache
.
apiCache
==
nil
{
cache
.
apiCache
=
&
sync
.
Map
{}
}
if
cache
.
antigravityCache
==
nil
{
cache
.
antigravityCache
=
&
sync
.
Map
{}
}
if
cache
.
windowStatsCache
==
nil
{
cache
.
windowStatsCache
=
&
sync
.
Map
{}
}
var
quotaFetcher
QuotaFetcher
if
antigravityQuotaFetcher
!=
nil
{
quotaFetcher
=
antigravityQuotaFetcher
}
return
&
AccountUsageService
{
accountRepo
:
accountRepo
,
usageLogRepo
:
usageLogRepo
,
usageFetcher
:
usageFetcher
,
geminiQuotaService
:
geminiQuotaService
,
antigravityQuotaFetcher
:
q
uotaFetcher
,
antigravityQuotaFetcher
:
antigravityQ
uotaFetcher
,
cache
:
cache
,
}
}
...
...
@@ -292,8 +265,8 @@ func (s *AccountUsageService) getGeminiUsage(ctx context.Context, account *Accou
totals
:=
geminiAggregateUsage
(
stats
)
resetAt
:=
geminiDailyResetTime
(
now
)
usage
.
GeminiProDaily
=
buildGeminiUsageProgress
(
totals
.
ProRequests
,
quota
.
ProRPD
,
resetAt
,
totals
.
ProTokens
,
totals
.
ProCost
)
usage
.
GeminiFlashDaily
=
buildGeminiUsageProgress
(
totals
.
FlashRequests
,
quota
.
FlashRPD
,
resetAt
,
totals
.
FlashTokens
,
totals
.
FlashCost
)
usage
.
GeminiProDaily
=
buildGeminiUsageProgress
(
totals
.
ProRequests
,
quota
.
ProRPD
,
resetAt
,
totals
.
ProTokens
,
totals
.
ProCost
,
now
)
usage
.
GeminiFlashDaily
=
buildGeminiUsageProgress
(
totals
.
FlashRequests
,
quota
.
FlashRPD
,
resetAt
,
totals
.
FlashTokens
,
totals
.
FlashCost
,
now
)
return
usage
,
nil
}
...
...
@@ -305,41 +278,20 @@ func (s *AccountUsageService) getAntigravityUsage(ctx context.Context, account *
return
&
UsageInfo
{
UpdatedAt
:
&
now
},
nil
}
// Ensure project_id is stable for quota queries.
if
strings
.
TrimSpace
(
account
.
GetCredential
(
"project_id"
))
==
""
{
projectID
:=
antigravity
.
GenerateMockProjectID
()
if
account
.
Credentials
==
nil
{
account
.
Credentials
=
map
[
string
]
any
{}
}
account
.
Credentials
[
"project_id"
]
=
projectID
if
s
.
accountRepo
!=
nil
{
_
,
err
:=
s
.
accountRepo
.
BulkUpdate
(
ctx
,
[]
int64
{
account
.
ID
},
AccountBulkUpdate
{
Credentials
:
map
[
string
]
any
{
"project_id"
:
projectID
},
})
if
err
!=
nil
{
log
.
Printf
(
"Failed to persist antigravity project_id for account %d: %v"
,
account
.
ID
,
err
)
}
}
}
// 1. 检查缓存(10 分钟)
if
cached
,
ok
:=
s
.
cache
.
antigravityCache
.
Load
(
account
.
ID
);
ok
{
if
cache
,
ok
:=
cached
.
(
*
antigravityUsageCache
);
ok
&&
time
.
Since
(
cache
.
timestamp
)
<
apiCacheTTL
{
// 重新计算 RemainingSeconds
usage
:=
cloneUsageInfo
(
cache
.
usageInfo
)
usage
:=
cache
.
usageInfo
if
usage
.
FiveHour
!=
nil
&&
usage
.
FiveHour
.
ResetsAt
!=
nil
{
usage
.
FiveHour
.
RemainingSeconds
=
remainingSeconds
Until
(
*
usage
.
FiveHour
.
ResetsAt
)
usage
.
FiveHour
.
RemainingSeconds
=
int
(
time
.
Until
(
*
usage
.
FiveHour
.
ResetsAt
)
.
Seconds
())
}
return
usage
,
nil
}
}
// 2. 获取代理 URL
proxyURL
,
err
:=
s
.
antigravityQuotaFetcher
.
GetProxyURL
(
ctx
,
account
)
if
err
!=
nil
{
log
.
Printf
(
"Failed to get proxy URL for account %d: %v"
,
account
.
ID
,
err
)
proxyURL
=
""
}
proxyURL
:=
s
.
antigravityQuotaFetcher
.
GetProxyURL
(
ctx
,
account
)
// 3. 调用 API 获取额度
result
,
err
:=
s
.
antigravityQuotaFetcher
.
FetchQuota
(
ctx
,
account
,
proxyURL
)
...
...
@@ -468,12 +420,12 @@ func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedA
// 5小时窗口 - 始终创建对象(即使 ResetsAt 为空)
info
.
FiveHour
=
&
UsageProgress
{
Utilization
:
clampFloat64
(
resp
.
FiveHour
.
Utilization
,
0
,
100
),
Utilization
:
resp
.
FiveHour
.
Utilization
,
}
if
resp
.
FiveHour
.
ResetsAt
!=
""
{
if
fiveHourReset
,
err
:=
parseTime
(
resp
.
FiveHour
.
ResetsAt
);
err
==
nil
{
info
.
FiveHour
.
ResetsAt
=
&
fiveHourReset
info
.
FiveHour
.
RemainingSeconds
=
remainingSeconds
Until
(
fiveHourReset
)
info
.
FiveHour
.
RemainingSeconds
=
int
(
time
.
Until
(
fiveHourReset
)
.
Seconds
())
}
else
{
log
.
Printf
(
"Failed to parse FiveHour.ResetsAt: %s, error: %v"
,
resp
.
FiveHour
.
ResetsAt
,
err
)
}
...
...
@@ -483,14 +435,14 @@ func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedA
if
resp
.
SevenDay
.
ResetsAt
!=
""
{
if
sevenDayReset
,
err
:=
parseTime
(
resp
.
SevenDay
.
ResetsAt
);
err
==
nil
{
info
.
SevenDay
=
&
UsageProgress
{
Utilization
:
clampFloat64
(
resp
.
SevenDay
.
Utilization
,
0
,
100
),
Utilization
:
resp
.
SevenDay
.
Utilization
,
ResetsAt
:
&
sevenDayReset
,
RemainingSeconds
:
remainingSeconds
Until
(
sevenDayReset
),
RemainingSeconds
:
int
(
time
.
Until
(
sevenDayReset
)
.
Seconds
())
,
}
}
else
{
log
.
Printf
(
"Failed to parse SevenDay.ResetsAt: %s, error: %v"
,
resp
.
SevenDay
.
ResetsAt
,
err
)
info
.
SevenDay
=
&
UsageProgress
{
Utilization
:
clampFloat64
(
resp
.
SevenDay
.
Utilization
,
0
,
100
),
Utilization
:
resp
.
SevenDay
.
Utilization
,
}
}
}
...
...
@@ -499,14 +451,14 @@ func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedA
if
resp
.
SevenDaySonnet
.
ResetsAt
!=
""
{
if
sonnetReset
,
err
:=
parseTime
(
resp
.
SevenDaySonnet
.
ResetsAt
);
err
==
nil
{
info
.
SevenDaySonnet
=
&
UsageProgress
{
Utilization
:
clampFloat64
(
resp
.
SevenDaySonnet
.
Utilization
,
0
,
100
),
Utilization
:
resp
.
SevenDaySonnet
.
Utilization
,
ResetsAt
:
&
sonnetReset
,
RemainingSeconds
:
remainingSeconds
Until
(
sonnetReset
),
RemainingSeconds
:
int
(
time
.
Until
(
sonnetReset
)
.
Seconds
())
,
}
}
else
{
log
.
Printf
(
"Failed to parse SevenDaySonnet.ResetsAt: %s, error: %v"
,
resp
.
SevenDaySonnet
.
ResetsAt
,
err
)
info
.
SevenDaySonnet
=
&
UsageProgress
{
Utilization
:
clampFloat64
(
resp
.
SevenDaySonnet
.
Utilization
,
0
,
100
),
Utilization
:
resp
.
SevenDaySonnet
.
Utilization
,
}
}
}
...
...
@@ -520,7 +472,10 @@ func (s *AccountUsageService) estimateSetupTokenUsage(account *Account) *UsageIn
// 如果有session_window信息
if
account
.
SessionWindowEnd
!=
nil
{
remaining
:=
remainingSecondsUntil
(
*
account
.
SessionWindowEnd
)
remaining
:=
int
(
time
.
Until
(
*
account
.
SessionWindowEnd
)
.
Seconds
())
if
remaining
<
0
{
remaining
=
0
}
// 根据状态估算使用率 (百分比形式,100 = 100%)
var
utilization
float64
...
...
@@ -532,7 +487,6 @@ func (s *AccountUsageService) estimateSetupTokenUsage(account *Account) *UsageIn
default
:
utilization
=
0.0
}
utilization
=
clampFloat64
(
utilization
,
0
,
100
)
info
.
FiveHour
=
&
UsageProgress
{
Utilization
:
utilization
,
...
...
@@ -551,12 +505,15 @@ func (s *AccountUsageService) estimateSetupTokenUsage(account *Account) *UsageIn
return
info
}
func
buildGeminiUsageProgress
(
used
,
limit
int64
,
resetAt
time
.
Time
,
tokens
int64
,
cost
float64
)
*
UsageProgress
{
func
buildGeminiUsageProgress
(
used
,
limit
int64
,
resetAt
time
.
Time
,
tokens
int64
,
cost
float64
,
now
time
.
Time
)
*
UsageProgress
{
if
limit
<=
0
{
return
nil
}
utilization
:=
clampFloat64
((
float64
(
used
)
/
float64
(
limit
))
*
100
,
0
,
100
)
remainingSeconds
:=
remainingSecondsUntil
(
resetAt
)
utilization
:=
(
float64
(
used
)
/
float64
(
limit
))
*
100
remainingSeconds
:=
int
(
resetAt
.
Sub
(
now
)
.
Seconds
())
if
remainingSeconds
<
0
{
remainingSeconds
=
0
}
resetCopy
:=
resetAt
return
&
UsageProgress
{
Utilization
:
utilization
,
...
...
@@ -569,47 +526,3 @@ func buildGeminiUsageProgress(used, limit int64, resetAt time.Time, tokens int64
},
}
}
func
cloneUsageInfo
(
src
*
UsageInfo
)
*
UsageInfo
{
if
src
==
nil
{
return
nil
}
dst
:=
*
src
if
src
.
UpdatedAt
!=
nil
{
t
:=
*
src
.
UpdatedAt
dst
.
UpdatedAt
=
&
t
}
dst
.
FiveHour
=
cloneUsageProgress
(
src
.
FiveHour
)
dst
.
SevenDay
=
cloneUsageProgress
(
src
.
SevenDay
)
dst
.
SevenDaySonnet
=
cloneUsageProgress
(
src
.
SevenDaySonnet
)
dst
.
GeminiProDaily
=
cloneUsageProgress
(
src
.
GeminiProDaily
)
dst
.
GeminiFlashDaily
=
cloneUsageProgress
(
src
.
GeminiFlashDaily
)
if
src
.
AntigravityQuota
!=
nil
{
dst
.
AntigravityQuota
=
make
(
map
[
string
]
*
AntigravityModelQuota
,
len
(
src
.
AntigravityQuota
))
for
k
,
v
:=
range
src
.
AntigravityQuota
{
if
v
==
nil
{
dst
.
AntigravityQuota
[
k
]
=
nil
continue
}
copyVal
:=
*
v
dst
.
AntigravityQuota
[
k
]
=
&
copyVal
}
}
return
&
dst
}
func
cloneUsageProgress
(
src
*
UsageProgress
)
*
UsageProgress
{
if
src
==
nil
{
return
nil
}
dst
:=
*
src
if
src
.
ResetsAt
!=
nil
{
t
:=
*
src
.
ResetsAt
dst
.
ResetsAt
=
&
t
}
if
src
.
WindowStats
!=
nil
{
statsCopy
:=
*
src
.
WindowStats
dst
.
WindowStats
=
&
statsCopy
}
return
&
dst
}
backend/internal/service/antigravity_gateway_service.go
View file @
1f2ced89
...
...
@@ -64,7 +64,6 @@ type AntigravityGatewayService struct {
tokenProvider
*
AntigravityTokenProvider
rateLimitService
*
RateLimitService
httpUpstream
HTTPUpstream
settingService
*
SettingService
}
func
NewAntigravityGatewayService
(
...
...
@@ -73,14 +72,12 @@ func NewAntigravityGatewayService(
tokenProvider
*
AntigravityTokenProvider
,
rateLimitService
*
RateLimitService
,
httpUpstream
HTTPUpstream
,
settingService
*
SettingService
,
)
*
AntigravityGatewayService
{
return
&
AntigravityGatewayService
{
accountRepo
:
accountRepo
,
tokenProvider
:
tokenProvider
,
rateLimitService
:
rateLimitService
,
httpUpstream
:
httpUpstream
,
settingService
:
settingService
,
}
}
...
...
@@ -310,106 +307,6 @@ func (s *AntigravityGatewayService) unwrapV1InternalResponse(body []byte) ([]byt
return
body
,
nil
}
// isSignatureRelatedError 检测是否为 signature 相关的 400 错误
// 注意:不包含 "thinking" 关键词,避免误判消息格式错误为 signature 错误
func
isSignatureRelatedError
(
statusCode
int
,
body
[]
byte
)
bool
{
if
statusCode
!=
400
{
return
false
}
bodyStr
:=
strings
.
ToLower
(
string
(
body
))
keywords
:=
[]
string
{
"signature"
,
"thought_signature"
,
"thoughtsignature"
,
"invalid signature"
,
"signature validation"
,
}
for
_
,
keyword
:=
range
keywords
{
if
strings
.
Contains
(
bodyStr
,
keyword
)
{
return
true
}
}
return
false
}
// isModelNotFoundError 检测是否为模型不存在的 404 错误
func
isModelNotFoundError
(
statusCode
int
,
body
[]
byte
)
bool
{
if
statusCode
!=
404
{
return
false
}
bodyStr
:=
strings
.
ToLower
(
string
(
body
))
keywords
:=
[]
string
{
"model not found"
,
"model does not exist"
,
"unknown model"
,
"invalid model"
,
}
for
_
,
keyword
:=
range
keywords
{
if
strings
.
Contains
(
bodyStr
,
keyword
)
{
return
true
}
}
return
false
}
// stripThinkingFromClaudeRequest 从 Claude 请求中移除有问题的 thinking 块
// 策略:只移除历史消息中带 dummy signature 的 thinking 块,保留本次 thinking 配置
// 这样可以让本次对话仍然使用 thinking 功能,只是清理历史中可能导致问题的内容
func
stripThinkingFromClaudeRequest
(
req
*
antigravity
.
ClaudeRequest
)
*
antigravity
.
ClaudeRequest
{
// 创建副本
stripped
:=
*
req
// 保留 thinking 配置,让本次对话仍然可以使用 thinking
// stripped.Thinking = nil // 不再移除
// 只移除消息中带 dummy signature 的 thinking 块
if
len
(
stripped
.
Messages
)
>
0
{
newMessages
:=
make
([]
antigravity
.
ClaudeMessage
,
0
,
len
(
stripped
.
Messages
))
for
_
,
msg
:=
range
stripped
.
Messages
{
newMsg
:=
msg
// 如果 content 是数组,过滤有问题的 thinking 块
var
blocks
[]
map
[
string
]
any
if
err
:=
json
.
Unmarshal
(
msg
.
Content
,
&
blocks
);
err
==
nil
{
filtered
:=
make
([]
map
[
string
]
any
,
0
,
len
(
blocks
))
for
_
,
block
:=
range
blocks
{
// 跳过带 dummy signature 的 thinking 块
if
blockType
,
ok
:=
block
[
"type"
]
.
(
string
);
ok
&&
blockType
==
"thinking"
{
if
sig
,
ok
:=
block
[
"signature"
]
.
(
string
);
ok
{
// 移除 dummy signature 的 thinking 块
if
sig
==
"skip_thought_signature_validator"
||
sig
==
""
{
continue
}
}
else
{
// 没有 signature 字段的 thinking 块也移除
continue
}
}
// 跳过没有 type 但有 thinking 字段的块(untyped thinking blocks)
if
_
,
hasType
:=
block
[
"type"
];
!
hasType
{
if
_
,
hasThinking
:=
block
[
"thinking"
];
hasThinking
{
continue
}
}
filtered
=
append
(
filtered
,
block
)
}
if
newContent
,
err
:=
json
.
Marshal
(
filtered
);
err
==
nil
{
newMsg
.
Content
=
newContent
}
}
newMessages
=
append
(
newMessages
,
newMsg
)
}
stripped
.
Messages
=
newMessages
}
return
&
stripped
}
// Forward 转发 Claude 协议请求(Claude → Gemini 转换)
func
(
s
*
AntigravityGatewayService
)
Forward
(
ctx
context
.
Context
,
c
*
gin
.
Context
,
account
*
Account
,
body
[]
byte
)
(
*
ForwardResult
,
error
)
{
startTime
:=
time
.
Now
()
...
...
@@ -505,70 +402,11 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
respBody
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
resp
.
Body
,
2
<<
20
))
s
.
handleUpstreamError
(
ctx
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
)
// Auto 模式:检测 signature 错误并自动降级重试
if
isSignatureRelatedError
(
resp
.
StatusCode
,
respBody
)
&&
claudeReq
.
Thinking
!=
nil
{
log
.
Printf
(
"[Antigravity] Detected signature-related error, retrying without thinking blocks (account: %s, model: %s)"
,
account
.
Name
,
mappedModel
)
// 关闭原始响应,释放连接(respBody 已读取到内存)
_
=
resp
.
Body
.
Close
()
// 移除 thinking 块并重试一次
strippedReq
:=
stripThinkingFromClaudeRequest
(
&
claudeReq
)
strippedBody
,
err
:=
antigravity
.
TransformClaudeToGemini
(
strippedReq
,
projectID
,
mappedModel
)
if
err
!=
nil
{
log
.
Printf
(
"[Antigravity] Failed to transform stripped request: %v"
,
err
)
// 降级失败,返回原始错误
if
s
.
shouldFailoverWithTempUnsched
(
ctx
,
account
,
resp
.
StatusCode
,
respBody
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
}
return
nil
,
s
.
writeMappedClaudeError
(
c
,
resp
.
StatusCode
,
respBody
)
}
// 发送降级请求
retryReq
,
err
:=
antigravity
.
NewAPIRequest
(
ctx
,
action
,
accessToken
,
strippedBody
)
if
err
!=
nil
{
log
.
Printf
(
"[Antigravity] Failed to create retry request: %v"
,
err
)
if
s
.
shouldFailoverWithTempUnsched
(
ctx
,
account
,
resp
.
StatusCode
,
respBody
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
}
return
nil
,
s
.
writeMappedClaudeError
(
c
,
resp
.
StatusCode
,
respBody
)
}
retryResp
,
err
:=
s
.
httpUpstream
.
Do
(
retryReq
,
proxyURL
,
account
.
ID
,
account
.
Concurrency
)
if
err
!=
nil
{
log
.
Printf
(
"[Antigravity] Retry request failed: %v"
,
err
)
if
s
.
shouldFailoverWithTempUnsched
(
ctx
,
account
,
resp
.
StatusCode
,
respBody
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
}
return
nil
,
s
.
writeMappedClaudeError
(
c
,
resp
.
StatusCode
,
respBody
)
}
// 如果重试成功,使用重试的响应(不要 return,让后面的代码处理响应)
if
retryResp
.
StatusCode
<
400
{
log
.
Printf
(
"[Antigravity] Retry succeeded after stripping thinking blocks (account: %s, model: %s)"
,
account
.
Name
,
mappedModel
)
resp
=
retryResp
}
else
{
// 重试也失败,返回重试的错误
retryRespBody
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
retryResp
.
Body
,
2
<<
20
))
_
=
retryResp
.
Body
.
Close
()
log
.
Printf
(
"[Antigravity] Retry also failed with status %d: %s"
,
retryResp
.
StatusCode
,
string
(
retryRespBody
))
s
.
handleUpstreamError
(
ctx
,
account
,
retryResp
.
StatusCode
,
retryResp
.
Header
,
retryRespBody
)
if
s
.
shouldFailoverWithTempUnsched
(
ctx
,
account
,
retryResp
.
StatusCode
,
retryRespBody
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
retryResp
.
StatusCode
}
}
return
nil
,
s
.
writeMappedClaudeError
(
c
,
retryResp
.
StatusCode
,
retryRespBody
)
}
if
s
.
shouldFailoverUpstreamError
(
resp
.
StatusCode
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
}
// 不是 signature 错误,或者已经没有 thinking 块,直接返回错误
if
resp
.
StatusCode
>=
400
{
if
s
.
shouldFailoverWithTempUnsched
(
ctx
,
account
,
resp
.
StatusCode
,
respBody
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
}
return
nil
,
s
.
writeMappedClaudeError
(
c
,
resp
.
StatusCode
,
respBody
)
}
return
nil
,
s
.
writeMappedClaudeError
(
c
,
resp
.
StatusCode
,
respBody
)
}
requestID
:=
resp
.
Header
.
Get
(
"x-request-id"
)
...
...
@@ -620,7 +458,16 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
case
"generateContent"
,
"streamGenerateContent"
:
// ok
case
"countTokens"
:
return
nil
,
s
.
writeGoogleError
(
c
,
http
.
StatusNotImplemented
,
"countTokens is not supported"
)
// 直接返回空值,不透传上游
c
.
JSON
(
http
.
StatusOK
,
map
[
string
]
any
{
"totalTokens"
:
0
})
return
&
ForwardResult
{
RequestID
:
""
,
Usage
:
ClaudeUsage
{},
Model
:
originalModel
,
Stream
:
false
,
Duration
:
time
.
Since
(
time
.
Now
()),
FirstTokenMs
:
nil
,
},
nil
default
:
return
nil
,
s
.
writeGoogleError
(
c
,
http
.
StatusNotFound
,
"Unsupported action: "
+
action
)
}
...
...
@@ -713,42 +560,7 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
respBody
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
resp
.
Body
,
2
<<
20
))
s
.
handleUpstreamError
(
ctx
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
)
// Check if model fallback is enabled and this is a model not found error
if
s
.
settingService
!=
nil
&&
s
.
settingService
.
IsModelFallbackEnabled
(
ctx
)
&&
isModelNotFoundError
(
resp
.
StatusCode
,
respBody
)
{
fallbackModel
:=
s
.
settingService
.
GetFallbackModel
(
ctx
,
PlatformAntigravity
)
// Only retry if fallback model is different from current model
if
fallbackModel
!=
""
&&
fallbackModel
!=
mappedModel
{
log
.
Printf
(
"[Antigravity] Model not found (%s), retrying with fallback model %s (account: %s)"
,
mappedModel
,
fallbackModel
,
account
.
Name
)
// Close original response
_
=
resp
.
Body
.
Close
()
// Rebuild request with fallback model
fallbackBody
,
err
:=
s
.
wrapV1InternalRequest
(
projectID
,
fallbackModel
,
body
)
if
err
==
nil
{
fallbackReq
,
err
:=
antigravity
.
NewAPIRequest
(
ctx
,
upstreamAction
,
accessToken
,
fallbackBody
)
if
err
==
nil
{
fallbackResp
,
err
:=
s
.
httpUpstream
.
Do
(
fallbackReq
,
proxyURL
,
account
.
ID
,
account
.
Concurrency
)
if
err
==
nil
&&
fallbackResp
.
StatusCode
<
400
{
log
.
Printf
(
"[Antigravity] Fallback succeeded with %s (account: %s)"
,
fallbackModel
,
account
.
Name
)
resp
=
fallbackResp
originalModel
=
fallbackModel
// Update for billing
// Continue to normal response handling
goto
handleSuccess
}
else
if
fallbackResp
!=
nil
{
_
=
fallbackResp
.
Body
.
Close
()
}
}
}
log
.
Printf
(
"[Antigravity] Fallback failed, returning original error"
)
}
}
if
s
.
shouldFailoverWithTempUnsched
(
ctx
,
account
,
resp
.
StatusCode
,
respBody
)
{
if
s
.
shouldFailoverUpstreamError
(
resp
.
StatusCode
)
{
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
}
}
...
...
@@ -762,7 +574,6 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
return
nil
,
fmt
.
Errorf
(
"antigravity upstream error: %d"
,
resp
.
StatusCode
)
}
handleSuccess
:
var
usage
*
ClaudeUsage
var
firstTokenMs
*
int
...
...
@@ -813,15 +624,6 @@ func (s *AntigravityGatewayService) shouldFailoverUpstreamError(statusCode int)
}
}
func
(
s
*
AntigravityGatewayService
)
shouldFailoverWithTempUnsched
(
ctx
context
.
Context
,
account
*
Account
,
statusCode
int
,
body
[]
byte
)
bool
{
if
s
.
rateLimitService
!=
nil
{
if
s
.
rateLimitService
.
HandleTempUnschedulable
(
ctx
,
account
,
statusCode
,
body
)
{
return
true
}
}
return
s
.
shouldFailoverUpstreamError
(
statusCode
)
}
func
sleepAntigravityBackoff
(
attempt
int
)
{
sleepGeminiBackoff
(
attempt
)
// 复用 Gemini 的退避逻辑
}
...
...
@@ -932,10 +734,7 @@ func (s *AntigravityGatewayService) handleGeminiNonStreamingResponse(c *gin.Cont
}
// 解包 v1internal 响应
unwrapped
:=
respBody
if
inner
,
unwrapErr
:=
s
.
unwrapV1InternalResponse
(
respBody
);
unwrapErr
==
nil
&&
inner
!=
nil
{
unwrapped
=
inner
}
unwrapped
,
_
:=
s
.
unwrapV1InternalResponse
(
respBody
)
var
parsed
map
[
string
]
any
if
json
.
Unmarshal
(
unwrapped
,
&
parsed
)
==
nil
{
...
...
@@ -1009,8 +808,6 @@ func (s *AntigravityGatewayService) writeGoogleError(c *gin.Context, status int,
statusStr
=
"RESOURCE_EXHAUSTED"
case
500
:
statusStr
=
"INTERNAL"
case
501
:
statusStr
=
"UNIMPLEMENTED"
case
502
,
503
:
statusStr
=
"UNAVAILABLE"
}
...
...
backend/internal/service/antigravity_quota_fetcher.go
View file @
1f2ced89
...
...
@@ -2,7 +2,6 @@ package service
import
(
"context"
"fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
...
...
@@ -20,9 +19,6 @@ func NewAntigravityQuotaFetcher(proxyRepo ProxyRepository) *AntigravityQuotaFetc
// CanFetch 检查是否可以获取此账户的额度
func
(
f
*
AntigravityQuotaFetcher
)
CanFetch
(
account
*
Account
)
bool
{
if
f
==
nil
||
account
==
nil
{
return
false
}
if
account
.
Platform
!=
PlatformAntigravity
{
return
false
}
...
...
@@ -32,12 +28,6 @@ func (f *AntigravityQuotaFetcher) CanFetch(account *Account) bool {
// FetchQuota 获取 Antigravity 账户额度信息
func
(
f
*
AntigravityQuotaFetcher
)
FetchQuota
(
ctx
context
.
Context
,
account
*
Account
,
proxyURL
string
)
(
*
QuotaResult
,
error
)
{
if
f
==
nil
{
return
nil
,
fmt
.
Errorf
(
"antigravity quota fetcher is nil"
)
}
if
account
==
nil
{
return
nil
,
fmt
.
Errorf
(
"account is nil"
)
}
accessToken
:=
account
.
GetCredential
(
"access_token"
)
projectID
:=
account
.
GetCredential
(
"project_id"
)
...
...
@@ -71,10 +61,6 @@ func (f *AntigravityQuotaFetcher) buildUsageInfo(modelsResp *antigravity.FetchAv
AntigravityQuota
:
make
(
map
[
string
]
*
AntigravityModelQuota
),
}
if
modelsResp
==
nil
{
return
info
}
// 遍历所有模型,填充 AntigravityQuota
for
modelName
,
modelInfo
:=
range
modelsResp
.
Models
{
if
modelInfo
.
QuotaInfo
==
nil
{
...
...
@@ -82,7 +68,7 @@ func (f *AntigravityQuotaFetcher) buildUsageInfo(modelsResp *antigravity.FetchAv
}
// remainingFraction 是剩余比例 (0.0-1.0),转换为使用率百分比
utilization
:=
clampInt
(
int
((
1.0
-
modelInfo
.
QuotaInfo
.
RemainingFraction
)
*
100
),
0
,
100
)
utilization
:=
int
((
1.0
-
modelInfo
.
QuotaInfo
.
RemainingFraction
)
*
100
)
info
.
AntigravityQuota
[
modelName
]
=
&
AntigravityModelQuota
{
Utilization
:
utilization
,
...
...
@@ -94,14 +80,14 @@ func (f *AntigravityQuotaFetcher) buildUsageInfo(modelsResp *antigravity.FetchAv
priorityModels
:=
[]
string
{
"claude-sonnet-4-20250514"
,
"claude-sonnet-4"
,
"gemini-2.5-pro"
}
for
_
,
modelName
:=
range
priorityModels
{
if
modelInfo
,
ok
:=
modelsResp
.
Models
[
modelName
];
ok
&&
modelInfo
.
QuotaInfo
!=
nil
{
utilization
:=
clampFloat64
(
(
1.0
-
modelInfo
.
QuotaInfo
.
RemainingFraction
)
*
100
,
0
,
100
)
utilization
:=
(
1.0
-
modelInfo
.
QuotaInfo
.
RemainingFraction
)
*
100
progress
:=
&
UsageProgress
{
Utilization
:
utilization
,
}
if
modelInfo
.
QuotaInfo
.
ResetTime
!=
""
{
if
resetTime
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
modelInfo
.
QuotaInfo
.
ResetTime
);
err
==
nil
{
progress
.
ResetsAt
=
&
resetTime
progress
.
RemainingSeconds
=
remainingSeconds
Until
(
resetTime
)
progress
.
RemainingSeconds
=
int
(
time
.
Until
(
resetTime
)
.
Seconds
())
}
}
info
.
FiveHour
=
progress
...
...
@@ -113,22 +99,13 @@ func (f *AntigravityQuotaFetcher) buildUsageInfo(modelsResp *antigravity.FetchAv
}
// GetProxyURL 获取账户的代理 URL
func
(
f
*
AntigravityQuotaFetcher
)
GetProxyURL
(
ctx
context
.
Context
,
account
*
Account
)
(
string
,
error
)
{
if
f
==
nil
{
return
""
,
fmt
.
Errorf
(
"antigravity quota fetcher is nil"
)
}
if
account
==
nil
{
return
""
,
fmt
.
Errorf
(
"account is nil"
)
}
func
(
f
*
AntigravityQuotaFetcher
)
GetProxyURL
(
ctx
context
.
Context
,
account
*
Account
)
string
{
if
account
.
ProxyID
==
nil
||
f
.
proxyRepo
==
nil
{
return
""
,
nil
return
""
}
proxy
,
err
:=
f
.
proxyRepo
.
GetByID
(
ctx
,
*
account
.
ProxyID
)
if
err
!=
nil
{
return
""
,
err
}
if
proxy
==
nil
{
return
""
,
nil
if
err
!=
nil
||
proxy
==
nil
{
return
""
}
return
proxy
.
URL
()
,
nil
return
proxy
.
URL
()
}
backend/internal/service/quota_fetcher.go
View file @
1f2ced89
...
...
@@ -8,8 +8,6 @@ import (
type
QuotaFetcher
interface
{
// CanFetch 检查是否可以获取此账户的额度
CanFetch
(
account
*
Account
)
bool
// GetProxyURL 获取账户的代理 URL(如果没有代理则返回空字符串)
GetProxyURL
(
ctx
context
.
Context
,
account
*
Account
)
(
string
,
error
)
// FetchQuota 获取账户额度信息
FetchQuota
(
ctx
context
.
Context
,
account
*
Account
,
proxyURL
string
)
(
*
QuotaResult
,
error
)
}
...
...
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