mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 14:41:21 +00:00
feat: enhance text protocol conversion and advanced custom routing (#5825)
* refactor: consolidate relay protocol converters * refactor relayconvert text converters * feat: refine relay converters and advanced custom routing * refactor: enhance logging and add thought signature handling for Gemini requests * refactor: enhance channel cache and pricing endpoint handling for advanced custom models * feat: preserve billing usage semantics * feat: add protocol-aware billing usage * Delete useless files * chore: update action versions in workflow files * chore: update Docker action versions in workflow files * fix: harden billing usage settlement and hot-path route matching - estimate Gemini completion tokens locally when billable usageMetadata is prompt-only but output content was received (e.g. client aborts the stream before the final chunk), and rebuild the attached billing_usage as estimated so settlement does not bill zero output tokens - guard NewClaudeMessagesBillingUsage against all-zero ClaudeUsage, matching the OpenAI/Gemini constructors, so a zero billing_usage cannot override a non-zero top-level usage during settlement - cache compiled advanced-custom route model regexes; they run on the request hot path and were recompiled per request - move the effectiveBillingUsage remap to PostTextConsumeQuota only, and document that calculateTextQuotaSummary expects remapped usage - document the updatePricingLock -> channelSyncLock lock ordering that InitChannelCache/CacheUpdateChannel rely on, and the aux-struct pitfall in GeminiChatResponse.UnmarshalJSON
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
package dto
|
||||
|
||||
const (
|
||||
BillingUsageSourceClaudeMessages = "claude_messages"
|
||||
BillingUsageSourceGeminiChat = "gemini_chat"
|
||||
BillingUsageSourceOAIChat = "oai_chat"
|
||||
BillingUsageSourceOAIResponses = "oai_responses"
|
||||
|
||||
BillingUsageSemanticAnthropic = "anthropic"
|
||||
BillingUsageSemanticGemini = "gemini"
|
||||
BillingUsageSemanticOpenAI = "openai"
|
||||
)
|
||||
|
||||
type BillingUsage struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
Semantic string `json:"semantic,omitempty"`
|
||||
Estimated bool `json:"estimated,omitempty"`
|
||||
OpenAIUsage *Usage `json:"openai_usage,omitempty"`
|
||||
ClaudeUsage *ClaudeUsage `json:"claude_usage,omitempty"`
|
||||
GeminiUsageMetadata *GeminiUsageMetadata `json:"gemini_usage_metadata,omitempty"`
|
||||
}
|
||||
|
||||
func NewClaudeMessagesBillingUsage(usage *ClaudeUsage) *BillingUsage {
|
||||
if !HasClaudeUsageTokens(usage) {
|
||||
return nil
|
||||
}
|
||||
return &BillingUsage{
|
||||
Source: BillingUsageSourceClaudeMessages,
|
||||
Semantic: BillingUsageSemanticAnthropic,
|
||||
ClaudeUsage: cloneClaudeUsage(usage),
|
||||
}
|
||||
}
|
||||
|
||||
// HasClaudeUsageTokens mirrors HasOpenAIUsageTokens/HasGeminiUsageMetadataTokens:
|
||||
// an all-zero ClaudeUsage must not become a BillingUsage, otherwise it would take
|
||||
// precedence during settlement and zero out a non-zero top-level usage.
|
||||
func HasClaudeUsageTokens(usage *ClaudeUsage) bool {
|
||||
if usage == nil {
|
||||
return false
|
||||
}
|
||||
if usage.InputTokens != 0 ||
|
||||
usage.OutputTokens != 0 ||
|
||||
usage.CacheCreationInputTokens != 0 ||
|
||||
usage.CacheReadInputTokens != 0 ||
|
||||
usage.ClaudeCacheCreation5mTokens != 0 ||
|
||||
usage.ClaudeCacheCreation1hTokens != 0 {
|
||||
return true
|
||||
}
|
||||
if usage.CacheCreation != nil &&
|
||||
(usage.CacheCreation.Ephemeral5mInputTokens != 0 || usage.CacheCreation.Ephemeral1hInputTokens != 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewOpenAIChatBillingUsage(usage *Usage) *BillingUsage {
|
||||
return newOpenAIBillingUsage(BillingUsageSourceOAIChat, usage)
|
||||
}
|
||||
|
||||
func NewOpenAIResponsesBillingUsage(usage *Usage) *BillingUsage {
|
||||
return newOpenAIBillingUsage(BillingUsageSourceOAIResponses, usage)
|
||||
}
|
||||
|
||||
func newOpenAIBillingUsage(source string, usage *Usage) *BillingUsage {
|
||||
if !HasOpenAIUsageTokens(usage) {
|
||||
return nil
|
||||
}
|
||||
return &BillingUsage{
|
||||
Source: source,
|
||||
Semantic: BillingUsageSemanticOpenAI,
|
||||
OpenAIUsage: cloneOpenAIUsage(usage),
|
||||
}
|
||||
}
|
||||
|
||||
func HasOpenAIUsageTokens(usage *Usage) bool {
|
||||
if usage == nil {
|
||||
return false
|
||||
}
|
||||
if usage.PromptTokens != 0 ||
|
||||
usage.CompletionTokens != 0 ||
|
||||
usage.TotalTokens != 0 ||
|
||||
usage.InputTokens != 0 ||
|
||||
usage.OutputTokens != 0 ||
|
||||
usage.PromptCacheHitTokens != 0 ||
|
||||
usage.ClaudeCacheCreation5mTokens != 0 ||
|
||||
usage.ClaudeCacheCreation1hTokens != 0 {
|
||||
return true
|
||||
}
|
||||
if usage.PromptTokensDetails.CachedTokens != 0 ||
|
||||
usage.PromptTokensDetails.CachedCreationTokens != 0 ||
|
||||
usage.PromptTokensDetails.TextTokens != 0 ||
|
||||
usage.PromptTokensDetails.ImageTokens != 0 ||
|
||||
usage.PromptTokensDetails.AudioTokens != 0 {
|
||||
return true
|
||||
}
|
||||
if usage.CompletionTokenDetails.ReasoningTokens != 0 ||
|
||||
usage.CompletionTokenDetails.TextTokens != 0 ||
|
||||
usage.CompletionTokenDetails.ImageTokens != 0 ||
|
||||
usage.CompletionTokenDetails.AudioTokens != 0 {
|
||||
return true
|
||||
}
|
||||
return usage.InputTokensDetails != nil
|
||||
}
|
||||
|
||||
func NewGeminiChatBillingUsage(metadata *GeminiUsageMetadata) *BillingUsage {
|
||||
return newGeminiChatBillingUsage(metadata, false)
|
||||
}
|
||||
|
||||
func NewEstimatedGeminiChatBillingUsage(usage *Usage) *BillingUsage {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
totalTokens := usage.TotalTokens
|
||||
if totalTokens == 0 {
|
||||
totalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
}
|
||||
return newGeminiChatBillingUsage(&GeminiUsageMetadata{
|
||||
PromptTokenCount: usage.PromptTokens,
|
||||
CandidatesTokenCount: usage.CompletionTokens,
|
||||
TotalTokenCount: totalTokens,
|
||||
}, true)
|
||||
}
|
||||
|
||||
func newGeminiChatBillingUsage(metadata *GeminiUsageMetadata, estimated bool) *BillingUsage {
|
||||
if !HasGeminiUsageMetadataTokens(metadata) {
|
||||
return nil
|
||||
}
|
||||
usageMetadata := cloneGeminiUsageMetadata(*metadata)
|
||||
return &BillingUsage{
|
||||
Source: BillingUsageSourceGeminiChat,
|
||||
Semantic: BillingUsageSemanticGemini,
|
||||
Estimated: estimated,
|
||||
GeminiUsageMetadata: &usageMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
func CloneBillingUsage(usage *BillingUsage) *BillingUsage {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
clone := *usage
|
||||
clone.OpenAIUsage = cloneOpenAIUsage(usage.OpenAIUsage)
|
||||
clone.ClaudeUsage = cloneClaudeUsage(usage.ClaudeUsage)
|
||||
if usage.GeminiUsageMetadata != nil {
|
||||
metadata := cloneGeminiUsageMetadata(*usage.GeminiUsageMetadata)
|
||||
clone.GeminiUsageMetadata = &metadata
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
func cloneOpenAIUsage(usage *Usage) *Usage {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
clone := *usage
|
||||
clone.BillingUsage = nil
|
||||
if usage.InputTokensDetails != nil {
|
||||
inputTokensDetails := *usage.InputTokensDetails
|
||||
clone.InputTokensDetails = &inputTokensDetails
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
func cloneClaudeUsage(usage *ClaudeUsage) *ClaudeUsage {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
clone := *usage
|
||||
clone.BillingUsage = nil
|
||||
if usage.CacheCreation != nil {
|
||||
cacheCreation := *usage.CacheCreation
|
||||
clone.CacheCreation = &cacheCreation
|
||||
}
|
||||
if usage.ServerToolUse != nil {
|
||||
serverToolUse := *usage.ServerToolUse
|
||||
clone.ServerToolUse = &serverToolUse
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
func cloneGeminiUsageMetadata(metadata GeminiUsageMetadata) GeminiUsageMetadata {
|
||||
metadata.PromptTokensDetails = append([]GeminiPromptTokensDetails{}, metadata.PromptTokensDetails...)
|
||||
metadata.ToolUsePromptTokensDetails = append([]GeminiPromptTokensDetails{}, metadata.ToolUsePromptTokensDetails...)
|
||||
metadata.CandidatesTokensDetails = append([]GeminiPromptTokensDetails{}, metadata.CandidatesTokensDetails...)
|
||||
metadata.BillingUsage = nil
|
||||
return metadata
|
||||
}
|
||||
|
||||
func HasGeminiUsageMetadataTokens(metadata *GeminiUsageMetadata) bool {
|
||||
if metadata == nil {
|
||||
return false
|
||||
}
|
||||
if metadata.PromptTokenCount != 0 ||
|
||||
metadata.ToolUsePromptTokenCount != 0 ||
|
||||
metadata.CandidatesTokenCount != 0 ||
|
||||
metadata.TotalTokenCount != 0 ||
|
||||
metadata.ThoughtsTokenCount != 0 ||
|
||||
metadata.CachedContentTokenCount != 0 {
|
||||
return true
|
||||
}
|
||||
for _, detail := range metadata.PromptTokensDetails {
|
||||
if detail.TokenCount != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, detail := range metadata.ToolUsePromptTokensDetails {
|
||||
if detail.TokenCount != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, detail := range metadata.CandidatesTokensDetails {
|
||||
if detail.TokenCount != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user