package gateway import ( "testing" "llmsproxy/internal/types" ) // TestRecordChatUsageKeepsReasoningTokens pins the reasoning-token plumbing. // // The adapters already parse completion_tokens_details.reasoning_tokens and // forward it (opencodego, gemini, anthropic all do), and types.go has carried // the field since it was defined — but nothing ever READ it. The number arrived // in the response and was discarded at the one place that could have kept it, // so a pricing rule had no way to split thinking from output. // // The property that matters most: reasoning is a SUBSET of completion. Every // provider measured bills it at the completion rate, so a billing plugin that // added it to completion_tokens would double-charge every thinking token. The // test asserts completion is left alone. func TestRecordChatUsageKeepsReasoningTokens(t *testing.T) { rec := &Req{} resp := &types.UnifiedResponse{ TokenUsage: types.TokenUsage{ Prompt: 100, Completion: 500, CompletionTokensDetails: &types.CompletionTokensDetails{ ReasoningTokens: 320, }, }, } recordChatUsage(rec, &types.ChatRequest{}, resp) if rec.Reasoning != 320 { t.Errorf("Reasoning = %d, want 320", rec.Reasoning) } // The subset must not be folded into completion — it is already counted there. if rec.Compl != 500 { t.Errorf("Compl = %d, want 500 (reasoning is a subset, never additive)", rec.Compl) } if rec.Prompt != 100 { t.Errorf("Prompt = %d, want 100", rec.Prompt) } } // TestRecordChatUsageReasoningAbsentStaysZero: "not reported" and "reported as // zero" must both leave the field at 0, and neither may invent a number. A // plugin reading Reasoning cannot tell them apart, which is why the field is // documented as "0 means not reported" rather than "0 means no thinking". func TestRecordChatUsageReasoningAbsentStaysZero(t *testing.T) { for _, tc := range []struct { name string u types.TokenUsage }{ {"no details object", types.TokenUsage{Prompt: 10, Completion: 20}}, {"details without reasoning", types.TokenUsage{ Prompt: 10, Completion: 20, CompletionTokensDetails: &types.CompletionTokensDetails{}, }}, } { rec := &Req{} recordChatUsage(rec, &types.ChatRequest{}, &types.UnifiedResponse{TokenUsage: tc.u}) if rec.Reasoning != 0 { t.Errorf("%s: Reasoning = %d, want 0", tc.name, rec.Reasoning) } if rec.Compl != 20 { t.Errorf("%s: Compl = %d, want 20", tc.name, rec.Compl) } } }