Files
new-api/dto/billing_usage.go
T
CaIon 48068ce923 feat: bill OpenAI cache_write_tokens at cache-creation price with zero clamp
Parse OpenAI's native cache_write_tokens (chat prompt_tokens_details /
responses input_tokens_details), bill it at the cache-creation ratio, and
clamp the uncached prompt remainder at zero since cached + cache-write can
exceed prompt_tokens. Propagate the field through chat/responses/claude
format conversions and tiered expression billing (cc variable).
2026-07-11 21:18:49 +08:00

219 lines
6.4 KiB
Go

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.CacheWriteTokens != 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
}