| package executor |
|
|
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "strings" |
| "sync" |
| "time" |
|
|
| "github.com/gin-gonic/gin" |
| cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" |
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| type usageReporter struct { |
| provider string |
| model string |
| authID string |
| authIndex string |
| apiKey string |
| source string |
| requestedAt time.Time |
| once sync.Once |
| } |
|
|
| func newUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *usageReporter { |
| apiKey := apiKeyFromContext(ctx) |
| reporter := &usageReporter{ |
| provider: provider, |
| model: model, |
| requestedAt: time.Now(), |
| apiKey: apiKey, |
| source: resolveUsageSource(auth, apiKey), |
| } |
| 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) |
| } |
|
|
| func (r *usageReporter) publishFailure(ctx context.Context) { |
| r.publishWithOutcome(ctx, usage.Detail{}, true) |
| } |
|
|
| func (r *usageReporter) trackFailure(ctx context.Context, errPtr *error) { |
| if r == nil || errPtr == nil { |
| return |
| } |
| if *errPtr != nil { |
| r.publishFailure(ctx) |
| } |
| } |
|
|
| func (r *usageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool) { |
| if r == nil { |
| return |
| } |
| if detail.TotalTokens == 0 { |
| total := detail.InputTokens + detail.OutputTokens + detail.ReasoningTokens |
| if total > 0 { |
| detail.TotalTokens = total |
| } |
| } |
| if detail.InputTokens == 0 && detail.OutputTokens == 0 && detail.ReasoningTokens == 0 && detail.CachedTokens == 0 && detail.TotalTokens == 0 && !failed { |
| return |
| } |
| r.once.Do(func() { |
| usage.PublishRecord(ctx, usage.Record{ |
| Provider: r.provider, |
| Model: r.model, |
| Source: r.source, |
| APIKey: r.apiKey, |
| AuthID: r.authID, |
| AuthIndex: r.authIndex, |
| RequestedAt: r.requestedAt, |
| Failed: failed, |
| Detail: detail, |
| }) |
| }) |
| } |
|
|
| |
| |
| |
| |
| func (r *usageReporter) ensurePublished(ctx context.Context) { |
| if r == nil { |
| return |
| } |
| r.once.Do(func() { |
| usage.PublishRecord(ctx, usage.Record{ |
| Provider: r.provider, |
| Model: r.model, |
| Source: r.source, |
| APIKey: r.apiKey, |
| AuthID: r.authID, |
| AuthIndex: r.authIndex, |
| RequestedAt: r.requestedAt, |
| Failed: false, |
| Detail: usage.Detail{}, |
| }) |
| }) |
| } |
|
|
| 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("apiKey"); 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 parseCodexUsage(data []byte) (usage.Detail, bool) { |
| usageNode := gjson.ParseBytes(data).Get("response.usage") |
| if !usageNode.Exists() { |
| return usage.Detail{}, false |
| } |
| detail := usage.Detail{ |
| InputTokens: usageNode.Get("input_tokens").Int(), |
| OutputTokens: usageNode.Get("output_tokens").Int(), |
| TotalTokens: usageNode.Get("total_tokens").Int(), |
| } |
| if cached := usageNode.Get("input_tokens_details.cached_tokens"); cached.Exists() { |
| detail.CachedTokens = cached.Int() |
| } |
| if reasoning := usageNode.Get("output_tokens_details.reasoning_tokens"); reasoning.Exists() { |
| detail.ReasoningTokens = reasoning.Int() |
| } |
| return detail, true |
| } |
|
|
| func parseOpenAIUsage(data []byte) usage.Detail { |
| usageNode := gjson.ParseBytes(data).Get("usage") |
| if !usageNode.Exists() { |
| return usage.Detail{} |
| } |
| detail := usage.Detail{ |
| InputTokens: usageNode.Get("prompt_tokens").Int(), |
| OutputTokens: usageNode.Get("completion_tokens").Int(), |
| TotalTokens: usageNode.Get("total_tokens").Int(), |
| } |
| if cached := usageNode.Get("prompt_tokens_details.cached_tokens"); cached.Exists() { |
| detail.CachedTokens = cached.Int() |
| } |
| if reasoning := usageNode.Get("completion_tokens_details.reasoning_tokens"); 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 !usageNode.Exists() { |
| return usage.Detail{}, false |
| } |
| detail := usage.Detail{ |
| InputTokens: usageNode.Get("prompt_tokens").Int(), |
| OutputTokens: usageNode.Get("completion_tokens").Int(), |
| TotalTokens: usageNode.Get("total_tokens").Int(), |
| } |
| if cached := usageNode.Get("prompt_tokens_details.cached_tokens"); cached.Exists() { |
| detail.CachedTokens = cached.Int() |
| } |
| if reasoning := usageNode.Get("completion_tokens_details.reasoning_tokens"); reasoning.Exists() { |
| detail.ReasoningTokens = reasoning.Int() |
| } |
| return detail, true |
| } |
|
|
| func parseClaudeUsage(data []byte) usage.Detail { |
| usageNode := gjson.ParseBytes(data).Get("usage") |
| if !usageNode.Exists() { |
| return usage.Detail{} |
| } |
| detail := usage.Detail{ |
| InputTokens: usageNode.Get("input_tokens").Int(), |
| OutputTokens: usageNode.Get("output_tokens").Int(), |
| CachedTokens: usageNode.Get("cache_read_input_tokens").Int(), |
| } |
| if detail.CachedTokens == 0 { |
| |
| detail.CachedTokens = usageNode.Get("cache_creation_input_tokens").Int() |
| } |
| detail.TotalTokens = detail.InputTokens + detail.OutputTokens |
| return detail |
| } |
|
|
| 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 |
| } |
| detail := usage.Detail{ |
| InputTokens: usageNode.Get("input_tokens").Int(), |
| OutputTokens: usageNode.Get("output_tokens").Int(), |
| CachedTokens: usageNode.Get("cache_read_input_tokens").Int(), |
| } |
| if detail.CachedTokens == 0 { |
| detail.CachedTokens = usageNode.Get("cache_creation_input_tokens").Int() |
| } |
| detail.TotalTokens = detail.InputTokens + detail.OutputTokens |
| return detail, true |
| } |
|
|
| 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 parseGeminiCLIUsage(data []byte) usage.Detail { |
| usageNode := gjson.ParseBytes(data) |
| node := usageNode.Get("response.usageMetadata") |
| if !node.Exists() { |
| node = usageNode.Get("response.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 |
| } |
| node := gjson.GetBytes(payload, "response.usageMetadata") |
| if !node.Exists() { |
| node = gjson.GetBytes(payload, "usage_metadata") |
| } |
| if !node.Exists() { |
| return usage.Detail{}, false |
| } |
| return parseGeminiFamilyUsageDetail(node), true |
| } |
|
|
| func parseAntigravityUsage(data []byte) usage.Detail { |
| usageNode := gjson.ParseBytes(data) |
| node := usageNode.Get("response.usageMetadata") |
| if !node.Exists() { |
| node = usageNode.Get("usageMetadata") |
| } |
| if !node.Exists() { |
| node = usageNode.Get("usage_metadata") |
| } |
| if !node.Exists() { |
| return usage.Detail{} |
| } |
| return parseGeminiFamilyUsageDetail(node) |
| } |
|
|
| func parseAntigravityStreamUsage(line []byte) (usage.Detail, bool) { |
| payload := jsonPayload(line) |
| if len(payload) == 0 || !gjson.ValidBytes(payload) { |
| return usage.Detail{}, false |
| } |
| node := gjson.GetBytes(payload, "response.usageMetadata") |
| if !node.Exists() { |
| 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 |
| } |
|
|
| var stopChunkWithoutUsage sync.Map |
|
|
| func rememberStopWithoutUsage(traceID string) { |
| stopChunkWithoutUsage.Store(traceID, struct{}{}) |
| time.AfterFunc(10*time.Minute, func() { stopChunkWithoutUsage.Delete(traceID) }) |
| } |
|
|
| |
| |
| |
| func FilterSSEUsageMetadata(payload []byte) []byte { |
| if len(payload) == 0 { |
| return payload |
| } |
|
|
| lines := bytes.Split(payload, []byte("\n")) |
| modified := false |
| foundData := false |
| for idx, line := range lines { |
| trimmed := bytes.TrimSpace(line) |
| if len(trimmed) == 0 || !bytes.HasPrefix(trimmed, []byte("data:")) { |
| continue |
| } |
| foundData = true |
| dataIdx := bytes.Index(line, []byte("data:")) |
| if dataIdx < 0 { |
| continue |
| } |
| rawJSON := bytes.TrimSpace(line[dataIdx+5:]) |
| traceID := gjson.GetBytes(rawJSON, "traceId").String() |
| if isStopChunkWithoutUsage(rawJSON) && traceID != "" { |
| rememberStopWithoutUsage(traceID) |
| continue |
| } |
| if traceID != "" { |
| if _, ok := stopChunkWithoutUsage.Load(traceID); ok && hasUsageMetadata(rawJSON) { |
| stopChunkWithoutUsage.Delete(traceID) |
| continue |
| } |
| } |
|
|
| cleaned, changed := StripUsageMetadataFromJSON(rawJSON) |
| if !changed { |
| continue |
| } |
| var rebuilt []byte |
| rebuilt = append(rebuilt, line[:dataIdx]...) |
| rebuilt = append(rebuilt, []byte("data:")...) |
| if len(cleaned) > 0 { |
| rebuilt = append(rebuilt, ' ') |
| rebuilt = append(rebuilt, cleaned...) |
| } |
| lines[idx] = rebuilt |
| modified = true |
| } |
| if !modified { |
| if !foundData { |
| |
| trimmed := bytes.TrimSpace(payload) |
| cleaned, changed := StripUsageMetadataFromJSON(trimmed) |
| if !changed { |
| return payload |
| } |
| return cleaned |
| } |
| return payload |
| } |
| return bytes.Join(lines, []byte("\n")) |
| } |
|
|
| |
| |
| |
| |
| func StripUsageMetadataFromJSON(rawJSON []byte) ([]byte, bool) { |
| jsonBytes := bytes.TrimSpace(rawJSON) |
| if len(jsonBytes) == 0 || !gjson.ValidBytes(jsonBytes) { |
| return rawJSON, false |
| } |
|
|
| |
| finishReason := gjson.GetBytes(jsonBytes, "candidates.0.finishReason") |
| if !finishReason.Exists() { |
| finishReason = gjson.GetBytes(jsonBytes, "response.candidates.0.finishReason") |
| } |
| terminalReason := finishReason.Exists() && strings.TrimSpace(finishReason.String()) != "" |
|
|
| usageMetadata := gjson.GetBytes(jsonBytes, "usageMetadata") |
| if !usageMetadata.Exists() { |
| usageMetadata = gjson.GetBytes(jsonBytes, "response.usageMetadata") |
| } |
|
|
| |
| if terminalReason { |
| return rawJSON, false |
| } |
|
|
| |
| if !usageMetadata.Exists() { |
| return rawJSON, false |
| } |
|
|
| |
| cleaned := jsonBytes |
| var changed bool |
|
|
| if usageMetadata = gjson.GetBytes(cleaned, "usageMetadata"); usageMetadata.Exists() { |
| |
| cleaned, _ = sjson.SetRawBytes(cleaned, "cpaUsageMetadata", []byte(usageMetadata.Raw)) |
| cleaned, _ = sjson.DeleteBytes(cleaned, "usageMetadata") |
| changed = true |
| } |
|
|
| if usageMetadata = gjson.GetBytes(cleaned, "response.usageMetadata"); usageMetadata.Exists() { |
| |
| cleaned, _ = sjson.SetRawBytes(cleaned, "response.cpaUsageMetadata", []byte(usageMetadata.Raw)) |
| cleaned, _ = sjson.DeleteBytes(cleaned, "response.usageMetadata") |
| changed = true |
| } |
|
|
| return cleaned, changed |
| } |
|
|
| func hasUsageMetadata(jsonBytes []byte) bool { |
| if len(jsonBytes) == 0 || !gjson.ValidBytes(jsonBytes) { |
| return false |
| } |
| if gjson.GetBytes(jsonBytes, "usageMetadata").Exists() { |
| return true |
| } |
| if gjson.GetBytes(jsonBytes, "response.usageMetadata").Exists() { |
| return true |
| } |
| return false |
| } |
|
|
| func isStopChunkWithoutUsage(jsonBytes []byte) bool { |
| if len(jsonBytes) == 0 || !gjson.ValidBytes(jsonBytes) { |
| return false |
| } |
| finishReason := gjson.GetBytes(jsonBytes, "candidates.0.finishReason") |
| if !finishReason.Exists() { |
| finishReason = gjson.GetBytes(jsonBytes, "response.candidates.0.finishReason") |
| } |
| trimmed := strings.TrimSpace(finishReason.String()) |
| if !finishReason.Exists() || trimmed == "" { |
| return false |
| } |
| return !hasUsageMetadata(jsonBytes) |
| } |
|
|
| func jsonPayload(line []byte) []byte { |
| trimmed := bytes.TrimSpace(line) |
| if len(trimmed) == 0 { |
| return nil |
| } |
| if bytes.Equal(trimmed, []byte("[DONE]")) { |
| return nil |
| } |
| if bytes.HasPrefix(trimmed, []byte("event:")) { |
| return nil |
| } |
| if bytes.HasPrefix(trimmed, []byte("data:")) { |
| trimmed = bytes.TrimSpace(trimmed[len("data:"):]) |
| } |
| if len(trimmed) == 0 || trimmed[0] != '{' { |
| return nil |
| } |
| return trimmed |
| } |
|
|