702 lines · 19.2 KB · Go
RawDownload
package helps import (	"bytes"	"context"	"errors"	"fmt"	"strings"	"sync"	"time" 	"github.com/gin-gonic/gin"	internallogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"	cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"	"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"	"github.com/tidwall/gjson"	"github.com/tidwall/sjson") type UsageReporter struct {	provider    string	model       string	alias       string	authID      string	authIndex   string	authType    string	apiKey      string	source      string	reasoning   string	requestedAt time.Time	once        sync.Once} func NewUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *UsageReporter {	apiKey := APIKeyFromContext(ctx)	alias := usage.RequestedModelAliasFromContext(ctx)	if alias == "" {		alias = model	}	reporter := &UsageReporter{		provider:    provider,		model:       model,		alias:       strings.TrimSpace(alias),		requestedAt: time.Now(),		apiKey:      apiKey,		source:      resolveUsageSource(auth, apiKey),		authType:    resolveUsageAuthType(auth),		reasoning:   usage.ReasoningEffortFromContext(ctx),	}	if auth != nil {		reporter.authID = auth.ID		reporter.authIndex = auth.EnsureIndex()	}	return reporter} func (r *UsageReporter) Publish(ctx context.Context, detail usage.Detail) {	r.publishWithOutcome(ctx, detail, false, usage.Failure{})} func (r *UsageReporter) PublishAdditionalModel(ctx context.Context, model string, detail usage.Detail) {	record, ok := r.buildAdditionalModelRecord(model, detail)	if !ok {		return	}	r.publishRecord(ctx, record)} func (r *UsageReporter) buildAdditionalModelRecord(model string, detail usage.Detail) (usage.Record, bool) {	if r == nil {		return usage.Record{}, false	}	model = strings.TrimSpace(model)	if model == "" {		return usage.Record{}, false	}	detail = normalizeUsageDetailTotal(detail)	if !hasNonZeroTokenUsage(detail) {		return usage.Record{}, false	}	return r.buildRecordForModel(model, detail, false, usage.Failure{}), true} func (r *UsageReporter) PublishFailure(ctx context.Context, errs ...error) {	r.publishWithOutcome(ctx, usage.Detail{}, true, failFromErrors(errs...))} func (r *UsageReporter) TrackFailure(ctx context.Context, errPtr *error) {	if r == nil || errPtr == nil {		return	}	if *errPtr != nil {		r.PublishFailure(ctx, *errPtr)	}} func (r *UsageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool, fail usage.Failure) {	if r == nil {		return	}	detail = normalizeUsageDetailTotal(detail)	r.once.Do(func() {		r.publishRecord(ctx, r.buildRecord(detail, failed, fail))	})} func normalizeUsageDetailTotal(detail usage.Detail) usage.Detail {	if detail.TotalTokens == 0 {		total := detail.InputTokens + detail.OutputTokens + detail.ReasoningTokens		if total > 0 {			detail.TotalTokens = total		}	}	return detail} func hasNonZeroTokenUsage(detail usage.Detail) bool {	return detail.InputTokens != 0 ||		detail.OutputTokens != 0 ||		detail.ReasoningTokens != 0 ||		detail.CachedTokens != 0 ||		detail.CacheReadTokens != 0 ||		detail.CacheCreationTokens != 0 ||		detail.TotalTokens != 0} // ensurePublished guarantees that a usage record is emitted exactly once.// It is safe to call multiple times; only the first call wins due to once.Do.// This is used to ensure request counting even when upstream responses do not// include any usage fields (tokens), especially for streaming paths.func (r *UsageReporter) EnsurePublished(ctx context.Context) {	if r == nil {		return	}	r.once.Do(func() {		r.publishRecord(ctx, r.buildRecord(usage.Detail{}, false, usage.Failure{}))	})} func (r *UsageReporter) publishRecord(ctx context.Context, record usage.Record) {	record.ResponseHeaders = internallogging.GetResponseHeaders(ctx)	usage.PublishRecord(ctx, record)} func (r *UsageReporter) buildRecord(detail usage.Detail, failed bool, failures ...usage.Failure) usage.Record {	var fail usage.Failure	if len(failures) > 0 {		fail = failures[0]	}	if r == nil {		return usage.Record{Detail: detail, Failed: failed, Fail: fail}	}	return r.buildRecordForModel(r.model, detail, failed, fail)} func (r *UsageReporter) buildRecordForModel(model string, detail usage.Detail, failed bool, fail usage.Failure) usage.Record {	if r == nil {		return usage.Record{Model: model, Detail: detail, Failed: failed, Fail: fail}	}	return usage.Record{		Provider:        r.provider,		Model:           model,		Alias:           r.alias,		Source:          r.source,		APIKey:          r.apiKey,		AuthID:          r.authID,		AuthIndex:       r.authIndex,		AuthType:        r.authType,		ReasoningEffort: r.reasoning,		RequestedAt:     r.requestedAt,		Latency:         r.latency(),		Failed:          failed,		Fail:            fail,		Detail:          detail,	}} func failFromErrors(errs ...error) usage.Failure {	for _, err := range errs {		if err == nil {			continue		}		fail := usage.Failure{			Body: strings.TrimSpace(err.Error()),		}		var se interface{ StatusCode() int }		if errors.As(err, &se) && se != nil {			fail.StatusCode = se.StatusCode()		}		return fail	}	return usage.Failure{}} func (r *UsageReporter) latency() time.Duration {	if r == nil || r.requestedAt.IsZero() {		return 0	}	latency := time.Since(r.requestedAt)	if latency < 0 {		return 0	}	return latency} func APIKeyFromContext(ctx context.Context) string {	if ctx == nil {		return ""	}	ginCtx, ok := ctx.Value("gin").(*gin.Context)	if !ok || ginCtx == nil {		return ""	}	if v, exists := ginCtx.Get("userApiKey"); exists {		switch value := v.(type) {		case string:			return value		case fmt.Stringer:			return value.String()		default:			return fmt.Sprintf("%v", value)		}	}	return ""} func resolveUsageSource(auth *cliproxyauth.Auth, ctxAPIKey string) string {	if auth != nil {		provider := strings.TrimSpace(auth.Provider)		if strings.EqualFold(provider, "gemini-cli") {			if id := strings.TrimSpace(auth.ID); id != "" {				return id			}		}		if strings.EqualFold(provider, "vertex") {			if auth.Metadata != nil {				if projectID, ok := auth.Metadata["project_id"].(string); ok {					if trimmed := strings.TrimSpace(projectID); trimmed != "" {						return trimmed					}				}				if project, ok := auth.Metadata["project"].(string); ok {					if trimmed := strings.TrimSpace(project); trimmed != "" {						return trimmed					}				}			}		}		if _, value := auth.AccountInfo(); value != "" {			return strings.TrimSpace(value)		}		if auth.Metadata != nil {			if email, ok := auth.Metadata["email"].(string); ok {				if trimmed := strings.TrimSpace(email); trimmed != "" {					return trimmed				}			}		}		if auth.Attributes != nil {			if key := strings.TrimSpace(auth.Attributes["api_key"]); key != "" {				return key			}		}	}	if trimmed := strings.TrimSpace(ctxAPIKey); trimmed != "" {		return trimmed	}	return ""} func resolveUsageAuthType(auth *cliproxyauth.Auth) string {	if auth == nil {		return ""	}	kind, _ := auth.AccountInfo()	kind = strings.TrimSpace(kind)	if kind == "api_key" {		return "apikey"	}	return kind} func ParseCodexUsage(data []byte) (usage.Detail, bool) {	usageNode := gjson.ParseBytes(data).Get("response.usage")	if !hasOpenAIStyleUsageTokenFields(usageNode) {		return usage.Detail{}, false	}	return parseOpenAIStyleUsageNode(usageNode), true} func ParseCodexImageToolUsage(data []byte) (usage.Detail, bool) {	usageNode := gjson.ParseBytes(data).Get("response.tool_usage.image_gen")	if !hasOpenAIStyleUsageTokenFields(usageNode) {		return usage.Detail{}, false	}	return parseOpenAIStyleUsageNode(usageNode), true} func ParseOpenAIUsage(data []byte) usage.Detail {	usageNode := gjson.ParseBytes(data).Get("usage")	if !hasOpenAIStyleUsageTokenFields(usageNode) {		return usage.Detail{}	}	return parseOpenAIStyleUsageNode(usageNode)} func hasOpenAIStyleUsageTokenFields(usageNode gjson.Result) bool {	if !usageNode.Exists() || !usageNode.IsObject() {		return false	}	return usageNode.Get("prompt_tokens").Exists() ||		usageNode.Get("input_tokens").Exists() ||		usageNode.Get("completion_tokens").Exists() ||		usageNode.Get("output_tokens").Exists() ||		usageNode.Get("total_tokens").Exists() ||		usageNode.Get("prompt_tokens_details.cached_tokens").Exists() ||		usageNode.Get("input_tokens_details.cached_tokens").Exists() ||		usageNode.Get("completion_tokens_details.reasoning_tokens").Exists() ||		usageNode.Get("output_tokens_details.reasoning_tokens").Exists()} func parseOpenAIStyleUsageNode(usageNode gjson.Result) usage.Detail {	inputNode := usageNode.Get("prompt_tokens")	if !inputNode.Exists() {		inputNode = usageNode.Get("input_tokens")	}	outputNode := usageNode.Get("completion_tokens")	if !outputNode.Exists() {		outputNode = usageNode.Get("output_tokens")	}	detail := usage.Detail{		InputTokens:  inputNode.Int(),		OutputTokens: outputNode.Int(),		TotalTokens:  usageNode.Get("total_tokens").Int(),	}	cached := usageNode.Get("prompt_tokens_details.cached_tokens")	if !cached.Exists() {		cached = usageNode.Get("input_tokens_details.cached_tokens")	}	if cached.Exists() {		detail.CachedTokens = cached.Int()	}	reasoning := usageNode.Get("completion_tokens_details.reasoning_tokens")	if !reasoning.Exists() {		reasoning = usageNode.Get("output_tokens_details.reasoning_tokens")	}	if reasoning.Exists() {		detail.ReasoningTokens = reasoning.Int()	}	return detail} func ParseOpenAIStreamUsage(line []byte) (usage.Detail, bool) {	payload := jsonPayload(line)	if len(payload) == 0 || !gjson.ValidBytes(payload) {		return usage.Detail{}, false	}	usageNode := gjson.GetBytes(payload, "usage")	if !hasOpenAIStyleUsageTokenFields(usageNode) {		return usage.Detail{}, false	}	return parseOpenAIStyleUsageNode(usageNode), true} func ParseClaudeUsage(data []byte) usage.Detail {	usageNode := gjson.ParseBytes(data).Get("usage")	if !usageNode.Exists() {		return usage.Detail{}	}	return parseClaudeUsageNode(usageNode)} func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) {	payload := jsonPayload(line)	if len(payload) == 0 || !gjson.ValidBytes(payload) {		return usage.Detail{}, false	}	usageNode := gjson.GetBytes(payload, "usage")	if !usageNode.Exists() {		return usage.Detail{}, false	}	return parseClaudeUsageNode(usageNode), true} func parseClaudeUsageNode(usageNode gjson.Result) usage.Detail {	cacheReadTokens := usageNode.Get("cache_read_input_tokens").Int()	cacheCreationTokens := usageNode.Get("cache_creation_input_tokens").Int()	detail := usage.Detail{		InputTokens:         usageNode.Get("input_tokens").Int(),		OutputTokens:        usageNode.Get("output_tokens").Int(),		CachedTokens:        cacheReadTokens,		CacheReadTokens:     cacheReadTokens,		CacheCreationTokens: cacheCreationTokens,	}	if detail.CachedTokens == 0 {		detail.CachedTokens = detail.CacheCreationTokens	}	detail.TotalTokens = detail.InputTokens + detail.OutputTokens	return detail} func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail {	detail := usage.Detail{		InputTokens:     node.Get("promptTokenCount").Int(),		OutputTokens:    node.Get("candidatesTokenCount").Int(),		ReasoningTokens: node.Get("thoughtsTokenCount").Int(),		TotalTokens:     node.Get("totalTokenCount").Int(),		CachedTokens:    node.Get("cachedContentTokenCount").Int(),	}	if detail.TotalTokens == 0 {		detail.TotalTokens = detail.InputTokens + detail.OutputTokens + detail.ReasoningTokens	}	return detail} func hasGeminiFamilyUsageTokenFields(node gjson.Result) bool {	return node.Get("promptTokenCount").Exists() ||		node.Get("candidatesTokenCount").Exists() ||		node.Get("thoughtsTokenCount").Exists() ||		node.Get("totalTokenCount").Exists() ||		node.Get("cachedContentTokenCount").Exists()} func ParseGeminiCLIUsage(data []byte) usage.Detail {	usageNode := gjson.ParseBytes(data)	node := firstExistingUsageNode(usageNode,		"response.usageMetadata",		"response.usage_metadata",		"usageMetadata",		"usage_metadata",	)	if !node.Exists() {		return usage.Detail{}	}	return parseGeminiFamilyUsageDetail(node)} func ParseGeminiUsage(data []byte) usage.Detail {	usageNode := gjson.ParseBytes(data)	node := usageNode.Get("usageMetadata")	if !node.Exists() {		node = usageNode.Get("usage_metadata")	}	if !node.Exists() {		return usage.Detail{}	}	return parseGeminiFamilyUsageDetail(node)} func ParseGeminiStreamUsage(line []byte) (usage.Detail, bool) {	payload := jsonPayload(line)	if len(payload) == 0 || !gjson.ValidBytes(payload) {		return usage.Detail{}, false	}	node := gjson.GetBytes(payload, "usageMetadata")	if !node.Exists() {		node = gjson.GetBytes(payload, "usage_metadata")	}	if !node.Exists() {		return usage.Detail{}, false	}	return parseGeminiFamilyUsageDetail(node), true} func ParseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) {	payload := jsonPayload(line)	if len(payload) == 0 || !gjson.ValidBytes(payload) {		return usage.Detail{}, false	}	root := gjson.ParseBytes(payload)	node := firstExistingUsageNode(root,		"response.usageMetadata",		"response.usage_metadata",		"usageMetadata",		"usage_metadata",	)	if !node.Exists() {		return usage.Detail{}, false	}	if !hasGeminiFamilyUsageTokenFields(node) {		return usage.Detail{}, false	}	return parseGeminiFamilyUsageDetail(node), true} func firstExistingUsageNode(root gjson.Result, paths ...string) gjson.Result {	for _, path := range paths {		node := root.Get(path)		if node.Exists() {			return node		}	}	return gjson.Result{}} func ParseAntigravityUsage(data []byte) usage.Detail {	usageNode := gjson.ParseBytes(data)	node := usageNode.Get("response.usageMetadata")	if !node.Exists() {		node = usageNode.Get("usageMetadata")

Lines 1–500 of 702.