token accounting cleanup
This commit is contained in:
+132
@@ -1943,3 +1943,135 @@ func TestSanitizeMessagesWithInvisibles(t *testing.T) {
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------- new coverage from review ----------
|
||||
|
||||
// #14: subagent token usage must be accumulated into the parent turn usage.
|
||||
func TestALRunSubagentAccumulatesUsage(t *testing.T) {
|
||||
var n int
|
||||
var mu sync.Mutex
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
n++
|
||||
cur := n
|
||||
mu.Unlock()
|
||||
switch cur {
|
||||
case 1:
|
||||
w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"c1","type":"function","function":{"name":"run_subagent","arguments":"{\"prompt\":\"inner\"}"}}]}}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15,"prompt_tokens_details":{"cached_tokens":7}}}`))
|
||||
case 2:
|
||||
w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"child done"}}],"usage":{"prompt_tokens":20,"completion_tokens":3,"total_tokens":23,"prompt_tokens_details":{"cached_tokens":12}}}`))
|
||||
case 3:
|
||||
w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"parent done"}}],"usage":{"prompt_tokens":30,"completion_tokens":4,"total_tokens":34,"prompt_tokens_details":{"cached_tokens":15}}}`))
|
||||
default:
|
||||
t.Errorf("unexpected request #%d", cur)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cfg := defCfg
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
_, usg, err := AL(context.Background(), &cfg, []Message{{Role: "system", Content: strp("sys")}, {Role: "user", Content: strp("parent task")}}, "sys", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("AL: %v", err)
|
||||
}
|
||||
// PromptTokens/Cached reflect the parent's own final context (30 / 15); they are
|
||||
// overwritten per iteration, not accumulated. Completion/Total accumulate every
|
||||
// assistant call: parent's tool-call call (5/15) + child (3/23) + parent final (4/34)
|
||||
// => completion 12, total 72.
|
||||
if usg.PromptTokens != 30 {
|
||||
t.Errorf("PromptTokens = %d, want 30", usg.PromptTokens)
|
||||
}
|
||||
if usg.CompletionTokens != 12 {
|
||||
t.Errorf("CompletionTokens = %d, want 12", usg.CompletionTokens)
|
||||
}
|
||||
if usg.TotalTokens != 72 {
|
||||
t.Errorf("TotalTokens = %d, want 72", usg.TotalTokens)
|
||||
}
|
||||
if usg.Cached() != 15 {
|
||||
t.Errorf("CachedTokens = %d, want 15", usg.Cached())
|
||||
}
|
||||
}
|
||||
|
||||
// #7: invalid-assistant detection must match common provider error variants.
|
||||
func TestIsInvalidAssistantErrVariants(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"HTTP 400: {\"error\":\"Invalid assistant message: content or tool_calls must be set\"}", true},
|
||||
{"invalid assistant message: content or tool_calls must be set", true},
|
||||
{"content or tool_calls must be set", true},
|
||||
{"Assistant message content must be set", true},
|
||||
{"tool_calls must be set", true},
|
||||
{"rate limit exceeded", false},
|
||||
{"model overloaded", false},
|
||||
{"", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := isInvalidAssistantErr(errors.New(c.in)); got != c.want {
|
||||
t.Errorf("isInvalidAssistantErr(%q) = %v, want %v", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
if isInvalidAssistantErr(nil) {
|
||||
t.Errorf("isInvalidAssistantErr(nil) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
// #10: internalKey must reject all agent-internal params and allow forwarding extras.
|
||||
func TestInternalKey(t *testing.T) {
|
||||
internal := []string{"endpoint", "model", "temperature", "stream", "api_key", "timeout", "shell_timeout", "max_al_iterations", "color", "context_window"}
|
||||
for _, k := range internal {
|
||||
if !internalKey(k) {
|
||||
t.Errorf("internalKey(%q) = false, want true", k)
|
||||
}
|
||||
}
|
||||
extra := []string{"reasoning_effort", "top_p", "max_tokens", "stop", "frequency_penalty"}
|
||||
for _, k := range extra {
|
||||
if internalKey(k) {
|
||||
t.Errorf("internalKey(%q) = true, want false", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #17: the retry loop must make exactly len(fib)+1 attempts on persistent 5xx
|
||||
// (initial attempt + one retry per Fibonacci delay) and no extra attempt.
|
||||
func TestLLMRetryAttemptCount(t *testing.T) {
|
||||
var n int
|
||||
var mu sync.Mutex
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
n++
|
||||
mu.Unlock()
|
||||
w.WriteHeader(503)
|
||||
w.Write([]byte(`{"error":"unavailable"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cfg := defCfg
|
||||
cfg.Endpoint = srv.URL
|
||||
cfg.Stream = false
|
||||
cfg.APIKey = "-"
|
||||
_, _, err := llm(context.Background(), &cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error from persistent 5xx")
|
||||
}
|
||||
fib := []int{1, 1, 2, 3, 5, 8, 13, 21, 34}
|
||||
want := len(fib) + 1
|
||||
if n != want {
|
||||
t.Errorf("attempts = %d, want %d", n, want)
|
||||
}
|
||||
}
|
||||
|
||||
// #9: filterText keeps only printable runs plus ASCII space/tab/newline.
|
||||
func TestFilterTextPrintableOnly(t *testing.T) {
|
||||
in := "ok\t\n" + "a" + "\x00" + "\u200B" + "\u00A0" + "\r" + "b"
|
||||
got := filterText(in)
|
||||
if strings.ContainsAny(got, "\x00\r") || strings.Contains(got, "\u200B") || strings.Contains(got, "\u00A0") {
|
||||
t.Errorf("filterText left control/invisible chars: %q", got)
|
||||
}
|
||||
if got != "ok\t\nab" {
|
||||
t.Errorf("filterText = %q, want %q", got, "ok\t\nab")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user