627 lines · 23.9 KB · Go
RawDownload
package helps import (	"context"	"io"	"net/http"	"strings"	"testing"	"time" 	"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage") func TestParseOpenAIUsageChatCompletions(t *testing.T) {	data := []byte(`{"usage":{"prompt_tokens":10,"completion_tokens":6,"total_tokens":16,"prompt_tokens_details":{"cached_tokens":4},"completion_tokens_details":{"reasoning_tokens":5}}}`)	detail := ParseOpenAIUsage(data)	if detail.InputTokens != 10 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)	}	if detail.OutputTokens != 6 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 6)	}	if detail.TotalTokens != 16 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 16)	}	if detail.CachedTokens != 4 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 4)	}	if detail.CacheReadTokens != 4 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 4)	}	if detail.ReasoningTokens != 5 {		t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 5)	}	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityComplete {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}	if detail.TokenBreakdown.Input.UncachedTokens != 6 || detail.TokenBreakdown.Output.NonReasoningTokens != 1 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseOpenAIUsageResponses(t *testing.T) {	data := []byte(`{"service_tier":"default","usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30,"input_tokens_details":{"cached_tokens":7},"output_tokens_details":{"reasoning_tokens":9}}}`)	detail := ParseOpenAIUsage(data)	if detail.InputTokens != 10 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)	}	if detail.OutputTokens != 20 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 20)	}	if detail.TotalTokens != 30 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 30)	}	if detail.CachedTokens != 7 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 7)	}	if detail.CacheReadTokens != 7 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 7)	}	if detail.ReasoningTokens != 9 {		t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 9)	}	if detail.ResponseServiceTier != "default" {		t.Fatalf("response service tier = %q, want default", detail.ResponseServiceTier)	}	if detail.TokenBreakdown.Input.UncachedTokens != 3 || detail.TokenBreakdown.Output.NonReasoningTokens != 11 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseOpenAIUsageTotalOnlyIsUnclassified(t *testing.T) {	detail := ParseOpenAIUsage([]byte(`{"usage":{"total_tokens":42}}`))	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityUnclassified ||		detail.TotalTokens != 42 || detail.TokenBreakdown.UnclassifiedTokens != 42 {		t.Fatalf("detail = %+v", detail)	}} func TestParseOpenAIUsagePartialBucketsPreserveKnownTokens(t *testing.T) {	detail := ParseOpenAIUsage([]byte(`{"usage":{"input_tokens":10,"total_tokens":15}}`))	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityUnclassified ||		detail.TokenBreakdown.Input.TotalTokens != 10 || detail.TokenBreakdown.UnclassifiedTokens != 5 {		t.Fatalf("detail = %+v", detail)	}} func TestParseOpenAIUsageExplicitZeroBucketsRemainInconsistent(t *testing.T) {	detail := ParseOpenAIUsage([]byte(`{"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":42}}`))	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityInconsistent {		t.Fatalf("detail = %+v", detail)	}} func TestParseCodexUsageIncludesCacheWriteTokens(t *testing.T) {	data := []byte(`{"response":{"service_tier":"priority","usage":{"input_tokens":100,"output_tokens":20,"total_tokens":120,"input_tokens_details":{"cached_tokens":30,"cache_write_tokens":40}}}}`)	detail, ok := ParseCodexUsage(data)	if !ok {		t.Fatal("ParseCodexUsage() ok = false, want true")	}	if detail.InputTokens != 100 {		t.Fatalf("input tokens = %d, want 100", detail.InputTokens)	}	if detail.OutputTokens != 20 {		t.Fatalf("output tokens = %d, want 20", detail.OutputTokens)	}	if detail.CachedTokens != 30 {		t.Fatalf("cached tokens = %d, want 30", detail.CachedTokens)	}	if detail.CacheReadTokens != 30 {		t.Fatalf("cache read tokens = %d, want 30", detail.CacheReadTokens)	}	if detail.CacheCreationTokens != 40 {		t.Fatalf("cache creation tokens = %d, want 40", detail.CacheCreationTokens)	}	if detail.TotalTokens != 120 {		t.Fatalf("total tokens = %d, want 120", detail.TotalTokens)	}	if detail.ResponseServiceTier != "priority" {		t.Fatalf("response service tier = %q, want priority", detail.ResponseServiceTier)	}	if detail.TokenBreakdown.Input.UncachedTokens != 30 || detail.TokenBreakdown.Input.CacheWriteTokens != 40 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseOpenAIUsageNormalizesCacheCreationAlias(t *testing.T) {	data := []byte(`{"usage":{"input_tokens":10,"output_tokens":2,"total_tokens":12,"input_tokens_details":{"cache_creation_tokens":4}}}`)	detail := ParseOpenAIUsage(data)	if detail.CacheCreationTokens != 4 {		t.Fatalf("cache creation tokens = %d, want 4", detail.CacheCreationTokens)	}} func TestParseOpenAIUsageIgnoresNullUsage(t *testing.T) {	data := []byte(`{"usage":null}`)	detail := ParseOpenAIUsage(data)	if detail != (usage.Detail{}) {		t.Fatalf("detail = %+v, want zero detail", detail)	}} func TestParseOpenAIUsagePreservesResponseTierWithoutUsage(t *testing.T) {	t.Parallel() 	detail := ParseOpenAIUsage([]byte(`{"service_tier":"default"}`))	if detail.ResponseServiceTier != "default" {		t.Fatalf("response service tier = %q, want default", detail.ResponseServiceTier)	}} func TestParseCodexUsagePreservesResponseTierWithoutUsage(t *testing.T) {	t.Parallel() 	detail, ok := ParseCodexUsage([]byte(`{"response":{"service_tier":"default"}}`))	if !ok || detail.ResponseServiceTier != "default" {		t.Fatalf("ParseCodexUsage() = (%+v, %v), want response tier default", detail, ok)	}} func TestParseOpenAIStreamUsageIgnoresNullUsage(t *testing.T) {	line := []byte(`data: {"id":"chunk_1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":null}],"usage":null}`)	if detail, ok := ParseOpenAIStreamUsage(line); ok {		t.Fatalf("ParseOpenAIStreamUsage() = (%+v, true), want false for null usage", detail)	}} func TestParseOpenAIStreamUsageResponsesFields(t *testing.T) {	line := []byte(`data: {"id":"chunk_1","object":"chat.completion.chunk","service_tier":"flex","choices":[],"usage":{"input_tokens":8,"output_tokens":5,"total_tokens":13,"input_tokens_details":{"cached_tokens":3},"output_tokens_details":{"reasoning_tokens":2}}}`)	detail, ok := ParseOpenAIStreamUsage(line)	if !ok {		t.Fatal("ParseOpenAIStreamUsage() ok = false, want true")	}	if detail.InputTokens != 8 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 8)	}	if detail.OutputTokens != 5 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 5)	}	if detail.TotalTokens != 13 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 13)	}	if detail.CachedTokens != 3 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 3)	}	if detail.CacheReadTokens != 3 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 3)	}	if detail.ReasoningTokens != 2 {		t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 2)	}	if detail.ResponseServiceTier != "flex" {		t.Fatalf("response service tier = %q, want flex", detail.ResponseServiceTier)	}} func TestStreamUsageBufferKeepsLastUsage(t *testing.T) {	var buffer StreamUsageBuffer	buffer.Observe(usage.Detail{}, true)	buffer.Observe(usage.Detail{InputTokens: 1, OutputTokens: 1, TotalTokens: 2}, false)	buffer.Observe(usage.Detail{InputTokens: 39320, OutputTokens: 26, TotalTokens: 39346, CachedTokens: 33280}, true) 	detail, ok := buffer.Detail()	if !ok {		t.Fatal("buffer detail ok = false, want true")	}	if detail.InputTokens != 39320 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 39320)	}	if detail.OutputTokens != 26 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 26)	}	if detail.TotalTokens != 39346 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 39346)	}	if detail.CachedTokens != 33280 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 33280)	}} func TestStreamUsageBufferPreservesTierAcrossChunks(t *testing.T) {	t.Parallel() 	var buffer StreamUsageBuffer	buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"default"}`))	buffer.ObserveOpenAIStream([]byte(`data: {"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`))	detail, ok := buffer.Detail()	if !ok {		t.Fatal("Detail() ok = false, want true")	}	if detail.InputTokens != 1 || detail.OutputTokens != 1 || detail.ResponseServiceTier != "default" {		t.Fatalf("detail = %+v, want usage with response tier default", detail)	}} func TestStreamUsageBufferObserveOpenAIStreamStateTransitions(t *testing.T) {	t.Parallel() 	t.Run("same chunk", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"flex","usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}`))		detail, ok := buffer.Detail()		if !ok || detail.InputTokens != 2 || detail.ResponseServiceTier != "flex" {			t.Fatalf("detail = %+v ok=%v", detail, ok)		}	}) 	t.Run("usage before tier", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}`))		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"default"}`))		detail, ok := buffer.Detail()		if !ok || detail.InputTokens != 2 || detail.ResponseServiceTier != "default" {			t.Fatalf("detail = %+v ok=%v", detail, ok)		}	}) 	t.Run("final usage tier overrides early tier", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"default"}`))		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"priority","usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}`))		detail, ok := buffer.Detail()		if !ok || detail.ResponseServiceTier != "priority" {			t.Fatalf("detail = %+v ok=%v", detail, ok)		}	}) 	t.Run("irrelevant and invalid chunks do not change state", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"content":"the word \"usage\" appears here"}`))		buffer.ObserveOpenAIStream([]byte(`data: {"usage":`))		buffer.ObserveOpenAIStream([]byte(`data: {"usage":null}`))		if detail, ok := buffer.Detail(); ok {			t.Fatalf("detail = %+v ok=true, want empty buffer", detail)		}	}) 	t.Run("zero token usage is retained", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":0}}`))		if _, ok := buffer.Detail(); !ok {			t.Fatal("Detail() ok = false, want true")		}	})} func TestStreamUsageBufferPreservesOnlyZeroUsage(t *testing.T) {	var buffer StreamUsageBuffer	buffer.Observe(usage.Detail{}, true) 	detail, ok := buffer.Detail()	if !ok {		t.Fatal("buffer detail ok = false, want true")	}	if detail != (usage.Detail{}) {		t.Fatalf("detail = %+v, want zero detail", detail)	}} func TestParseClaudeUsageIncludesCacheTokensInTotal(t *testing.T) {	data := []byte(`{"usage":{"input_tokens":3085,"output_tokens":253,"cache_read_input_tokens":7,"cache_creation_input_tokens":19514}}`)	detail := ParseClaudeUsage(data)	if detail.InputTokens != 3085 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 3085)	}	if detail.OutputTokens != 253 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 253)	}	if detail.CacheReadTokens != 7 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 7)	}	if detail.CacheCreationTokens != 19514 {		t.Fatalf("cache creation tokens = %d, want %d", detail.CacheCreationTokens, 19514)	}	if detail.CachedTokens != 7 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 7)	}	if detail.TotalTokens != 22859 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 22859)	}	if detail.TokenBreakdown.Input.TotalTokens != 22606 || detail.TokenBreakdown.Input.UncachedTokens != 3085 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseClaudeUsageFallsBackCachedTokensToCacheCreation(t *testing.T) {	data := []byte(`{"usage":{"input_tokens":3085,"output_tokens":253,"cache_creation_input_tokens":19514}}`)	detail := ParseClaudeUsage(data)	if detail.CachedTokens != 19514 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 19514)	}	if detail.TotalTokens != 22852 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 22852)	}} func TestParseGeminiUsageNormalizesCachedContent(t *testing.T) {	detail := ParseGeminiUsage([]byte(`{"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":2,"cachedContentTokenCount":4,"totalTokenCount":12}}`))	if detail.CachedTokens != 4 {		t.Fatalf("cached tokens = %d, want 4", detail.CachedTokens)	}	if detail.CacheReadTokens != 4 {		t.Fatalf("cache read tokens = %d, want 4", detail.CacheReadTokens)	}	if detail.TokenBreakdown.Input.UncachedTokens != 6 || detail.TokenBreakdown.TotalTokens != 12 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseGeminiUsageIncludesToolUsePromptTokens(t *testing.T) {	detail := ParseGeminiUsage([]byte(`{"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":2,"thoughtsTokenCount":3,"toolUsePromptTokenCount":5,"totalTokenCount":20}}`))	if detail.InputTokens != 15 || detail.TotalTokens != 20 {		t.Fatalf("detail = %+v", detail)	}	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityComplete ||		detail.TokenBreakdown.Input.UncachedTokens != 15 || detail.TokenBreakdown.Output.ReasoningTokens != 3 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseGeminiUsageRejectsInvalidToolUseSums(t *testing.T) {	tests := map[string]string{		"negative": `{"usageMetadata":{"promptTokenCount":10,"toolUsePromptTokenCount":-1,"totalTokenCount":10}}`,		"overflow": `{"usageMetadata":{"promptTokenCount":9223372036854775807,"toolUsePromptTokenCount":1,"totalTokenCount":9223372036854775807}}`,	}	for name, payload := range tests {		t.Run(name, func(t *testing.T) {			detail := ParseGeminiUsage([]byte(payload))			if detail.InputTokens < 0 || !detail.TokenBreakdown.Valid() ||				detail.TokenBreakdown.Quality != usage.TokenAccountingQualityInconsistent {				t.Fatalf("detail = %+v", detail)			}		})	}} func TestParseInteractionsUsage(t *testing.T) {	detail := ParseInteractionsUsage([]byte(`{"usage":{"input_tokens":3,"output_tokens":4,"reasoning_tokens":5,"cached_tokens":2}}`))	if detail.InputTokens != 3 {		t.Fatalf("input tokens = %d, want 3", detail.InputTokens)	}	if detail.OutputTokens != 4 {		t.Fatalf("output tokens = %d, want 4", detail.OutputTokens)	}	if detail.ReasoningTokens != 5 {		t.Fatalf("reasoning tokens = %d, want 5", detail.ReasoningTokens)	}	if detail.TotalTokens != 12 {		t.Fatalf("total tokens = %d, want 12", detail.TotalTokens)	}	if detail.CachedTokens != 2 {		t.Fatalf("cached tokens = %d, want 2", detail.CachedTokens)	}	if detail.CacheReadTokens != 2 {		t.Fatalf("cache read tokens = %d, want 2", detail.CacheReadTokens)	}	if detail.TokenBreakdown.Input.UncachedTokens != 1 || detail.TokenBreakdown.Output.TotalTokens != 9 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestNormalizeUsageDetailTotalDoesNotDoubleCountReasoning(t *testing.T) {	detail := normalizeUsageDetailTotal(usage.Detail{		InputTokens:     100,		OutputTokens:    30,		ReasoningTokens: 12,	}, "openai", "")	if detail.TotalTokens != 130 {		t.Fatalf("total tokens = %d, want 130", detail.TotalTokens)	}	if detail.TokenBreakdown.Quality != usage.TokenAccountingQualityComplete || detail.TokenBreakdown.Output.ReasoningTokens != 12 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseInteractionsUsageNormalizesCacheWriteAlias(t *testing.T) {	detail := ParseInteractionsUsage([]byte(`{"usage":{"input_tokens":3,"cache_write_tokens":2}}`))	if detail.CacheCreationTokens != 2 {		t.Fatalf("cache creation tokens = %d, want 2", detail.CacheCreationTokens)	}} func TestParseInteractionsUsageIncludesToolUseTokens(t *testing.T) {	detail := ParseInteractionsUsage([]byte(`{"usage":{"total_input_tokens":2,"total_output_tokens":6,"total_thought_tokens":3,"total_tool_use_tokens":4,"total_tokens":15}}`))	if detail.InputTokens != 6 || detail.OutputTokens != 6 || detail.ReasoningTokens != 3 || detail.TotalTokens != 15 {		t.Fatalf("detail = %+v", detail)	}	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityComplete ||		detail.TokenBreakdown.Input.UncachedTokens != 6 || detail.TokenBreakdown.Output.TotalTokens != 9 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseInteractionsStreamUsage(t *testing.T) {	detail, ok := ParseInteractionsStreamUsage([]byte(`{"type":"interaction.completed","interaction":{"usage":{"input_tokens":2,"output_tokens":6,"total_tokens":8}}}`))	if !ok {		t.Fatal("ParseInteractionsStreamUsage() ok = false, want true")	}	if detail.TotalTokens != 8 {		t.Fatalf("total tokens = %d, want 8", detail.TotalTokens)	}} func TestParseInteractionsStreamUsageOfficialMetadata(t *testing.T) {	detail, ok := ParseInteractionsStreamUsage([]byte(`data: {"event_type":"finish","metadata":{"total_usage":{"total_input_tokens":2,"total_output_tokens":6,"total_thought_tokens":3,"total_cached_tokens":1,"total_tokens":11}}}`))	if !ok {		t.Fatal("ParseInteractionsStreamUsage() ok = false, want true")	}	if detail.InputTokens != 2 {		t.Fatalf("input tokens = %d, want 2", detail.InputTokens)	}	if detail.OutputTokens != 6 {		t.Fatalf("output tokens = %d, want 6", detail.OutputTokens)	}	if detail.ReasoningTokens != 3 {		t.Fatalf("reasoning tokens = %d, want 3", detail.ReasoningTokens)	}	if detail.CachedTokens != 1 {		t.Fatalf("cached tokens = %d, want 1", detail.CachedTokens)	}	if detail.CacheReadTokens != 1 {		t.Fatalf("cache read tokens = %d, want 1", detail.CacheReadTokens)	}	if detail.TotalTokens != 11 {		t.Fatalf("total tokens = %d, want 11", detail.TotalTokens)	}} func TestUsageReporterBuildRecordIncludesLatency(t *testing.T) {	reporter := &UsageReporter{		provider:    "openai",		model:       "gpt-5.4",		requestedAt: time.Now().Add(-1500 * time.Millisecond),	} 	record := reporter.buildRecord(usage.Detail{TotalTokens: 3}, false)	if record.Latency < time.Second {		t.Fatalf("latency = %v, want >= 1s", record.Latency)	}	if record.Latency > 3*time.Second {		t.Fatalf("latency = %v, want <= 3s", record.Latency)	}} func TestUsageReporterTrackHTTPClientStartsTTFTBeforeRoundTrip(t *testing.T) {	delay := 40 * time.Millisecond	reporter := NewUsageReporter(context.Background(), "openai", "gpt-5.4", nil)	client := reporter.TrackHTTPClient(&http.Client{		Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {			time.Sleep(delay)			return &http.Response{				StatusCode: http.StatusOK,				Status:     "200 OK",				Header:     make(http.Header),				Body:       io.NopCloser(strings.NewReader("ok")),				Request:    req,			}, nil		}),	}) 

Lines 1–500 of 627.