Files
cogito/tools_cumulative_test.go
LocalAI [bot] fe7fd5de11 feat: expose cumulative token usage per ExecuteTools run (#55)
* feat(agent): expose AgentState.Background for spawned background agents

Add an exported Background flag on AgentState, set true for background
spawns (spawn_agent background=true) and false for foreground ones. This
lets embedders tell unattended background work apart from a foreground
sub-agent whose result is consumed inline — e.g. to auto-notify on
completion only for background agents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: add per-run token usage accumulator (CumulativeUsage)

Adds a counting LLM decorator and a CumulativeUsage field on Status so a
full ExecuteTools run's token usage can be summed and exposed. Preserves
StreamingLLM so wrapping does not disable streaming.

* refactor: harden counting stream forwarder and document usage limits

Buffer the forwarded stream channel and make the send context-aware to
match the client convention and prevent goroutine leaks. Document that
streaming-path usage is unpopulated by the bundled clients today.

* feat: expose cumulative token usage on the ExecuteTools result

Wraps the run LLM in a counting decorator and stamps the summed usage onto
the returned fragment's Status.CumulativeUsage via a deferred named return,
covering all exit paths. Each sub-agent run reports its own total.

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:23:19 +02:00

51 lines
1.8 KiB
Go

package cogito_test
import (
. "github.com/mudler/cogito"
"github.com/mudler/cogito/tests/mock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sashabaranov/go-openai"
)
var _ = Describe("ExecuteTools cumulative usage", func() {
It("sums token usage across every LLM call in the run", func() {
mockLLM := mock.NewMockOpenAIClient()
// One tool round then a final text answer => >= 2 CreateChatCompletion
// calls plus one Ask. Each configured call reports 100 total tokens.
mockLLM.AddCreateChatCompletionFunction("search", `{"query": "test"}`)
mockTool := mock.NewMockTool("search", "Search for information")
mock.SetRunResult(mockTool, "Result")
mockLLM.SetAskResponse("Final answer")
mockLLM.SetCreateChatCompletionResponse(openai.ChatCompletionResponse{
Choices: []openai.ChatCompletionChoice{
{Message: openai.ChatCompletionMessage{Role: "assistant", Content: "No more tools needed."}},
},
})
mockLLM.SetUsage(40, 60, 100)
mockLLM.SetUsage(40, 60, 100)
mockLLM.SetUsage(40, 60, 100)
fragment := NewEmptyFragment().AddMessage(UserMessageRole, "Task")
result, err := ExecuteTools(mockLLM, fragment, WithTools(mockTool))
Expect(err).ToNot(HaveOccurred())
// Expected = the total tokens of every usage entry the mock dispensed.
expected := 0
for i := 0; i < mockLLM.CreateChatCompletionUsageIndex; i++ {
expected += mockLLM.CreateChatCompletionUsage[i].TotalTokens
}
for i := 0; i < mockLLM.AskUsageIndex; i++ {
expected += mockLLM.AskUsage[i].TotalTokens
}
Expect(expected).To(BeNumerically(">", 100), "test must drive at least two billed calls")
Expect(result.Status.CumulativeUsage.TotalTokens).To(Equal(expected))
Expect(result.Status.CumulativeUsage.TotalTokens).To(
BeNumerically(">", result.Status.LastUsage.TotalTokens),
"cumulative must exceed the last single call",
)
})
})