From fea0b2e720e3d5c4d281cc01851a8af72d2f2319 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 14 Feb 2026 10:41:01 +0100 Subject: [PATCH 1/2] fix: nil pointer dereference when RateLimit present without Usage Move the RateLimit assignment inside the Usage nil-check block. When res.Usage is nil but res.RateLimit is non-nil (e.g. Anthropic with track_usage disabled), msgUsage stays nil and dereferencing it panics. Assisted-By: cagent --- pkg/runtime/runtime.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 52d27f977..2f7a22276 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -1093,9 +1093,9 @@ func (r *LocalRuntime) RunStream(ctx context.Context, sess *session.Session) <-c Cost: messageCost, Model: messageModel, } - } - if res.RateLimit != nil { - msgUsage.RateLimit = *res.RateLimit + if res.RateLimit != nil { + msgUsage.RateLimit = *res.RateLimit + } } addAgentMessage(sess, a, &assistantMessage, events) From 354759bd27fb114d0546c9407fa80dd5bbb581b7 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sat, 14 Feb 2026 10:41:53 +0100 Subject: [PATCH 2/2] fix: avoid double-counting session cost with cumulative usage providers Providers like Gemini emit cumulative token usage on every stream chunk. The previous code used sess.Cost += on each emission, re-adding the full cost every time. Track the cost already contributed by the current stream and only apply the delta so the session total stays correct. Assisted-By: cagent --- pkg/runtime/runtime.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 2f7a22276..74a27eba0 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -1270,6 +1270,7 @@ func (r *LocalRuntime) handleStream(ctx context.Context, stream chat.MessageStre var actualModelEventEmitted bool var messageUsage *chat.Usage var messageRateLimit *chat.RateLimit + var prevStreamCost float64 // cost contributed by previous usage emission in this stream modelID := getAgentModelID(a) toolCallIndex := make(map[string]int) // toolCallID -> index in toolCalls slice @@ -1292,11 +1293,12 @@ func (r *LocalRuntime) handleStream(ctx context.Context, stream chat.MessageStre messageUsage = response.Usage if m != nil && m.Cost != nil { - cost := float64(response.Usage.InputTokens)*m.Cost.Input + + streamCost := (float64(response.Usage.InputTokens)*m.Cost.Input + float64(response.Usage.OutputTokens)*m.Cost.Output + float64(response.Usage.CachedInputTokens)*m.Cost.CacheRead + - float64(response.Usage.CacheWriteTokens)*m.Cost.CacheWrite - sess.Cost += cost / 1e6 + float64(response.Usage.CacheWriteTokens)*m.Cost.CacheWrite) / 1e6 + sess.Cost += streamCost - prevStreamCost + prevStreamCost = streamCost } sess.InputTokens = response.Usage.InputTokens + response.Usage.CachedInputTokens + response.Usage.CacheWriteTokens