feat: comprehensive token usage tracking for V2
Add provider-specific usage details, fix streaming usage, and return usage from all high-level APIs (Chat.Send, Generate[T], Agent.Run). Breaking changes: - Chat.Send/SendMessage/SendWithImages now return (string, *Usage, error) - Generate[T]/GenerateWith[T] now return (T, *Usage, error) - Agent.Run/RunMessages now return (string, *Usage, error) New features: - Usage.Details map for provider-specific token breakdowns (reasoning, cached, audio, thoughts tokens) - OpenAI streaming now captures usage via StreamOptions.IncludeUsage - Google streaming now captures UsageMetadata from final chunk - UsageTracker.Details() for accumulated detail totals - ModelPricing and PricingRegistry for cost computation Closes #2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
// coder.AsTool("code", "Write and run code"),
|
||||
// )),
|
||||
// )
|
||||
// result, err := orchestrator.Run(ctx, "Build a fibonacci function in Go")
|
||||
// result, _, err := orchestrator.Run(ctx, "Build a fibonacci function in Go")
|
||||
package agent
|
||||
|
||||
import (
|
||||
@@ -64,13 +64,15 @@ func New(model *llm.Model, system string, opts ...Option) *Agent {
|
||||
|
||||
// Run executes the agent with a user prompt. Each call is a fresh conversation.
|
||||
// The agent loops tool calls automatically until it produces a text response.
|
||||
func (a *Agent) Run(ctx context.Context, prompt string) (string, error) {
|
||||
// Returns the text response, accumulated token usage, and any error.
|
||||
func (a *Agent) Run(ctx context.Context, prompt string) (string, *llm.Usage, error) {
|
||||
return a.RunMessages(ctx, []llm.Message{llm.UserMessage(prompt)})
|
||||
}
|
||||
|
||||
// RunMessages executes the agent with full message control.
|
||||
// Each call is a fresh conversation. The agent loops tool calls automatically.
|
||||
func (a *Agent) RunMessages(ctx context.Context, messages []llm.Message) (string, error) {
|
||||
// Returns the text response, accumulated token usage, and any error.
|
||||
func (a *Agent) RunMessages(ctx context.Context, messages []llm.Message) (string, *llm.Usage, error) {
|
||||
chat := llm.NewChat(a.model, a.reqOpts...)
|
||||
if a.system != "" {
|
||||
chat.SetSystem(a.system)
|
||||
@@ -107,7 +109,8 @@ type delegateParams struct {
|
||||
func (a *Agent) AsTool(name, description string) llm.Tool {
|
||||
return llm.Define[delegateParams](name, description,
|
||||
func(ctx context.Context, p delegateParams) (string, error) {
|
||||
return a.Run(ctx, p.Input)
|
||||
text, _, err := a.Run(ctx, p.Input)
|
||||
return text, err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,15 +29,6 @@ func (m *mockProvider) Stream(ctx context.Context, req provider.Request, events
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) lastRequest() provider.Request {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if len(m.requests) == 0 {
|
||||
return provider.Request{}
|
||||
}
|
||||
return m.requests[len(m.requests)-1]
|
||||
}
|
||||
|
||||
func newMockModel(fn func(ctx context.Context, req provider.Request) (provider.Response, error)) *llm.Model {
|
||||
mp := &mockProvider{completeFunc: fn}
|
||||
return llm.NewClient(mp).Model("mock-model")
|
||||
@@ -53,7 +44,7 @@ func TestAgent_Run(t *testing.T) {
|
||||
model := newSimpleMockModel("Hello from agent!")
|
||||
a := New(model, "You are a helpful assistant.")
|
||||
|
||||
result, err := a.Run(context.Background(), "Say hello")
|
||||
result, _, err := a.Run(context.Background(), "Say hello")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -83,7 +74,7 @@ func TestAgent_Run_WithTools(t *testing.T) {
|
||||
})
|
||||
|
||||
a := New(model, "You are helpful.", WithTools(llm.NewToolBox(tool)))
|
||||
result, err := a.Run(context.Background(), "Use the greet tool")
|
||||
result, _, err := a.Run(context.Background(), "Use the greet tool")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -147,7 +138,7 @@ func TestAgent_AsTool_ParentChild(t *testing.T) {
|
||||
)),
|
||||
)
|
||||
|
||||
result, err := parent.Run(context.Background(), "Tell me about Go generics")
|
||||
result, _, err := parent.Run(context.Background(), "Tell me about Go generics")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -169,7 +160,7 @@ func TestAgent_RunMessages(t *testing.T) {
|
||||
llm.UserMessage("Follow up"),
|
||||
}
|
||||
|
||||
result, err := a.RunMessages(context.Background(), messages)
|
||||
result, _, err := a.RunMessages(context.Background(), messages)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -187,7 +178,7 @@ func TestAgent_ContextCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
_, err := a.Run(ctx, "This should fail")
|
||||
_, _, err := a.Run(ctx, "This should fail")
|
||||
if err == nil {
|
||||
t.Fatal("expected error from cancelled context")
|
||||
}
|
||||
@@ -204,7 +195,7 @@ func TestAgent_WithRequestOptions(t *testing.T) {
|
||||
WithRequestOptions(llm.WithTemperature(0.3), llm.WithMaxTokens(100)),
|
||||
)
|
||||
|
||||
_, err := a.Run(context.Background(), "test")
|
||||
_, _, err := a.Run(context.Background(), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -224,7 +215,7 @@ func TestAgent_Run_Error(t *testing.T) {
|
||||
})
|
||||
a := New(model, "You are helpful.")
|
||||
|
||||
_, err := a.Run(context.Background(), "test")
|
||||
_, _, err := a.Run(context.Background(), "test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
@@ -234,7 +225,7 @@ func TestAgent_EmptySystem(t *testing.T) {
|
||||
model := newSimpleMockModel("no system prompt")
|
||||
a := New(model, "") // Empty system prompt
|
||||
|
||||
result, err := a.Run(context.Background(), "test")
|
||||
result, _, err := a.Run(context.Background(), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -242,3 +233,34 @@ func TestAgent_EmptySystem(t *testing.T) {
|
||||
t.Errorf("unexpected result: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_Run_ReturnsUsage(t *testing.T) {
|
||||
model := newMockModel(func(ctx context.Context, req provider.Request) (provider.Response, error) {
|
||||
return provider.Response{
|
||||
Text: "result",
|
||||
Usage: &provider.Usage{
|
||||
InputTokens: 100,
|
||||
OutputTokens: 50,
|
||||
TotalTokens: 150,
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
a := New(model, "You are helpful.")
|
||||
result, usage, err := a.Run(context.Background(), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result != "result" {
|
||||
t.Errorf("expected 'result', got %q", result)
|
||||
}
|
||||
if usage == nil {
|
||||
t.Fatal("expected usage, got nil")
|
||||
}
|
||||
if usage.InputTokens != 100 {
|
||||
t.Errorf("expected input 100, got %d", usage.InputTokens)
|
||||
}
|
||||
if usage.OutputTokens != 50 {
|
||||
t.Errorf("expected output 50, got %d", usage.OutputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func Example_researcher() {
|
||||
agent.WithRequestOptions(llm.WithTemperature(0.3)),
|
||||
)
|
||||
|
||||
result, err := researcher.Run(context.Background(), "What are the latest developments in Go generics?")
|
||||
result, _, err := researcher.Run(context.Background(), "What are the latest developments in Go generics?")
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
@@ -50,7 +50,7 @@ func Example_coder() {
|
||||
)),
|
||||
)
|
||||
|
||||
result, err := coder.Run(context.Background(),
|
||||
result, _, err := coder.Run(context.Background(),
|
||||
"Create a Go program that prints the first 10 Fibonacci numbers. Save it and run it.")
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
@@ -97,7 +97,7 @@ func Example_orchestrator() {
|
||||
)),
|
||||
)
|
||||
|
||||
result, err := orchestrator.Run(context.Background(),
|
||||
result, _, err := orchestrator.Run(context.Background(),
|
||||
"Research how to implement a binary search tree in Go, then create one with insert and search operations.")
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
|
||||
Reference in New Issue
Block a user