From 19a048879cc7b409c5d1782c4b8e1990c7503180 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Sat, 22 Nov 2025 14:04:28 +0800 Subject: [PATCH] feat(runtime): track antigravity usage and token counts --- .../runtime/executor/antigravity_executor.go | 5 ++ internal/runtime/executor/usage_helpers.go | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 477828d6..9fb591fa 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -104,6 +104,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au return resp, err } + reporter.publish(ctx, parseAntigravityUsage(bodyBytes)) var param any converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, bodyBytes, ¶m) resp = cliproxyexecutor.Response{Payload: []byte(converted)} @@ -177,6 +178,10 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya continue } + if detail, ok := parseAntigravityStreamUsage(payload); ok { + reporter.publish(ctx, detail) + } + chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, bytes.Clone(payload), ¶m) for i := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} diff --git a/internal/runtime/executor/usage_helpers.go b/internal/runtime/executor/usage_helpers.go index 94fc1cb1..d262acbe 100644 --- a/internal/runtime/executor/usage_helpers.go +++ b/internal/runtime/executor/usage_helpers.go @@ -365,6 +365,57 @@ func parseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) { return detail, 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{} + } + detail := usage.Detail{ + InputTokens: node.Get("promptTokenCount").Int(), + OutputTokens: node.Get("candidatesTokenCount").Int(), + ReasoningTokens: node.Get("thoughtsTokenCount").Int(), + TotalTokens: node.Get("totalTokenCount").Int(), + } + if detail.TotalTokens == 0 { + detail.TotalTokens = detail.InputTokens + detail.OutputTokens + detail.ReasoningTokens + } + return detail +} + +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 + } + detail := usage.Detail{ + InputTokens: node.Get("promptTokenCount").Int(), + OutputTokens: node.Get("candidatesTokenCount").Int(), + ReasoningTokens: node.Get("thoughtsTokenCount").Int(), + TotalTokens: node.Get("totalTokenCount").Int(), + } + if detail.TotalTokens == 0 { + detail.TotalTokens = detail.InputTokens + detail.OutputTokens + detail.ReasoningTokens + } + return detail, true +} + func jsonPayload(line []byte) []byte { trimmed := bytes.TrimSpace(line) if len(trimmed) == 0 {