forked from mudler/cogito
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathusage_counter.go
More file actions
130 lines (117 loc) · 4.51 KB
/
Copy pathusage_counter.go
File metadata and controls
130 lines (117 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package cogito
import (
"context"
"sync/atomic"
"github.com/sashabaranov/go-openai"
)
// usageCounter accumulates token usage across every LLM call routed through a
// countingLLM. Safe for concurrent use (sub-agents run in their own goroutines,
// each with its own counter, but streaming delivery may add from a goroutine).
type usageCounter struct {
prompt atomic.Int64
completion atomic.Int64
total atomic.Int64
}
func (c *usageCounter) add(u LLMUsage) {
c.prompt.Add(int64(u.PromptTokens))
c.completion.Add(int64(u.CompletionTokens))
c.total.Add(int64(u.TotalTokens))
}
func (c *usageCounter) snapshot() LLMUsage {
return LLMUsage{
PromptTokens: int(c.prompt.Load()),
CompletionTokens: int(c.completion.Load()),
TotalTokens: int(c.total.Load()),
}
}
// countingLLM wraps an LLM, accumulating token usage from every call into
// counter. CreateChatCompletion returns usage directly; Ask discards it from
// its signature but records it on the returned fragment's Status.LastUsage,
// which is where we read it.
type countingLLM struct {
LLM
counter *usageCounter
}
func (c *countingLLM) CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (LLMReply, LLMUsage, error) {
reply, usage, err := c.LLM.CreateChatCompletion(ctx, req)
if err == nil {
c.counter.add(usage)
}
return reply, usage, err
}
// Ask recovers per-call usage from the returned fragment's Status.LastUsage,
// which every cogito Ask implementation (and the test mock) refreshes on each
// call. If a future Ask returned a fragment carrying a stale LastUsage, this
// would re-add it — the assumption is that Ask always sets LastUsage fresh.
func (c *countingLLM) Ask(ctx context.Context, f Fragment) (Fragment, error) {
res, err := c.LLM.Ask(ctx, f)
if err == nil && res.Status != nil {
c.counter.add(res.Status.LastUsage)
}
return res, err
}
// SetPendingNativeParts forwards native multimodal parts to the wrapped LLM when
// it supports them, so wrapping for usage counting does not defeat the
// NativePartsAware seam used by the streaming/tool-decision request paths.
// countingLLM embeds the LLM interface, which does NOT promote NativePartsAware;
// without this forwarder the tools.go seams' llm.(NativePartsAware) assertions
// fail on the wrapped client and native audio/video is never serialized.
func (c *countingLLM) SetPendingNativeParts(parts []NativePart) {
if npa, ok := c.LLM.(NativePartsAware); ok {
npa.SetPendingNativeParts(parts)
}
}
// Compile-time assertions: the usage-counting wrappers must satisfy
// NativePartsAware so they forward native parts to the wrapped client.
// *countingStreamingLLM inherits SetPendingNativeParts via its embedded
// countingLLM value (pointer-receiver method promoted onto the pointer type).
var (
_ NativePartsAware = (*countingLLM)(nil)
_ NativePartsAware = (*countingStreamingLLM)(nil)
)
// countingStreamingLLM preserves StreamingLLM so wrapping does not disable the
// streaming code path for callers that use it. Usage is accumulated from the
// StreamEventDone event's Usage field.
//
// NOTE: cogito's bundled clients (clients/openai_client.go, clients/localai_client.go)
// do not currently populate StreamEvent.Usage on the done event, so streaming-path
// token accumulation is zero in production until those clients request usage from
// the API (e.g. StreamOptions{IncludeUsage: true}). The non-streaming path
// (CreateChatCompletion / Ask) is fully counted.
type countingStreamingLLM struct {
countingLLM
streaming StreamingLLM
}
func (c *countingStreamingLLM) CreateChatCompletionStream(ctx context.Context, req openai.ChatCompletionRequest) (<-chan StreamEvent, error) {
in, err := c.streaming.CreateChatCompletionStream(ctx, req)
if err != nil {
return nil, err
}
// Buffer to match the client convention (clients/openai_client.go) and make
// the forward context-aware so a stopped consumer cannot leak this goroutine.
out := make(chan StreamEvent, 64)
go func() {
defer close(out)
for ev := range in {
if ev.Type == StreamEventDone {
c.counter.add(ev.Usage)
}
select {
case out <- ev:
case <-ctx.Done():
return
}
}
}()
return out, nil
}
// newCountingLLM wraps llm so token usage accumulates into counter. When llm is
// streaming-capable, the returned wrapper is too, so the streaming path is
// preserved.
func newCountingLLM(llm LLM, counter *usageCounter) LLM {
base := countingLLM{LLM: llm, counter: counter}
if s, ok := llm.(StreamingLLM); ok {
return &countingStreamingLLM{countingLLM: base, streaming: s}
}
return &base
}