Repository navigation
Expand file tree
/
Copy pathsession_usage.go
More file actions
123 lines (115 loc) · 4.24 KB
/
Copy pathsession_usage.go
File metadata and controls
123 lines (115 loc) · 4.24 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
package codex
import (
"encoding/json"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
)
func (s *Session) beginUsageTurn(turnID string) {
s.usageMu.Lock()
defer s.usageMu.Unlock()
if s.usageTurnID == turnID {
return
}
s.usageTurnID = turnID
s.usageBaseline = s.usageTotal
s.latestUsage = nil
}
func (s *Session) onUsageUpdated(raw json.RawMessage) {
var p ThreadTokenUsageUpdatedNotification
if json.Unmarshal(raw, &p) != nil {
return
}
if !s.isRootThread(p.ThreadID) {
s.usageMu.Lock()
if s.resumeUsageThreadID != "" && p.ThreadID == s.resumeUsageThreadID && p.TurnID != "" && p.TokenUsage != nil && p.TokenUsage.Total != nil {
s.resumeUsageTotal = p.TokenUsage.Total
}
s.usageMu.Unlock()
return
}
if s.terminal.Load() || s.retiredTurns[p.TurnID] {
return
}
var observed *TurnUsage
s.usageMu.Lock()
defer func() {
s.usageMu.Unlock()
// Native notifications are ordered by the RPC reader. Publish before it
// reads completion, without holding the snapshot lock across backpressure.
if observed != nil {
s.emitUsage(*observed)
}
}()
if p.TokenUsage != nil && p.TokenUsage.Total != nil && p.TurnID != "" {
// app-server replays the previous thread total after resume and
// before turn/started. It establishes a baseline, not new usage.
if s.usageTurnID == "" {
s.usageTotal = *p.TokenUsage.Total
return
}
if p.TurnID != s.usageTurnID {
return
}
s.usageTotal = *p.TokenUsage.Total
u := subtractUsage(s.usageTotal, s.usageBaseline)
if u.InputTokens == 0 && u.OutputTokens == 0 && u.TotalTokens == 0 {
// The thread total has not advanced past this Turn's baseline. It
// reports no usage for this Turn, e.g. when native sends it after an
// interrupt before any response reported usage; publishing the
// difference would turn missing usage into measured zeros.
return
}
observed = &u
} else if p.Usage != nil && s.usageTurnID != "" && (p.TurnID == "" || p.TurnID == s.usageTurnID) {
u := *p.Usage
observed = &u
}
if observed != nil {
s.latestUsage = observed
}
}
func subtractUsage(total, baseline TurnUsage) TurnUsage {
return TurnUsage{
observed: true,
complete: total.completeTokens() && (!baseline.observed || baseline.completeTokens()) &&
total.InputTokens >= baseline.InputTokens && total.OutputTokens >= baseline.OutputTokens &&
total.CachedInputTokens >= baseline.CachedInputTokens &&
total.ReasoningOutputTokens >= baseline.ReasoningOutputTokens && total.TotalTokens >= baseline.TotalTokens,
ReasoningOutputTokens: max(0, total.ReasoningOutputTokens-baseline.ReasoningOutputTokens),
InputTokens: max(0, total.InputTokens-baseline.InputTokens),
OutputTokens: max(0, total.OutputTokens-baseline.OutputTokens),
CachedInputTokens: max(0, total.CachedInputTokens-baseline.CachedInputTokens),
CacheReadInputTokens: max(0, total.CacheReadInputTokens-baseline.CacheReadInputTokens),
TotalTokens: max(0, total.TotalTokens-baseline.TotalTokens),
}
}
func (u *TurnUsage) UnmarshalJSON(raw []byte) error {
type plain TurnUsage
var value plain
if err := json.Unmarshal(raw, &value); err != nil {
return err
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return err
}
*u = TurnUsage(value)
u.observed, u.complete = true, true
for _, key := range []string{"inputTokens", "outputTokens", "cachedInputTokens", "reasoningOutputTokens", "totalTokens"} {
var n *int64
if json.Unmarshal(fields[key], &n) != nil || n == nil || *n < 0 {
u.complete = false
}
}
return nil
}
func (u TurnUsage) completeTokens() bool {
return u.complete && u.CachedInputTokens <= u.InputTokens && u.ReasoningOutputTokens <= u.OutputTokens &&
u.InputTokens <= u.TotalTokens && u.OutputTokens == u.TotalTokens-u.InputTokens
}
func (s *Session) usagePayload(u TurnUsage) proto.Usage {
result := proto.Usage{Provider: "openai", Model: s.resolvedModel, InputTokens: int32(u.InputTokens), OutputTokens: int32(u.OutputTokens)}
if u.completeTokens() {
result.Tokens = &proto.TokenUsage{InputTokens: int64(u.InputTokens), OutputTokens: int64(u.OutputTokens), CachedInputTokens: int64(u.CachedInputTokens), ReasoningOutputTokens: int64(u.ReasoningOutputTokens), TotalTokens: int64(u.TotalTokens)}
}
return result
}