1174 lines
43 KiB
Go
1174 lines
43 KiB
Go
package control
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"gpt-load/internal/channel"
|
|
"gpt-load/internal/execution"
|
|
"gpt-load/internal/platform/config"
|
|
"gpt-load/internal/pricing"
|
|
"gpt-load/internal/protocol"
|
|
"gpt-load/internal/reasoning"
|
|
"gpt-load/internal/requestlog"
|
|
"gpt-load/internal/telemetry"
|
|
"gpt-load/internal/usage"
|
|
)
|
|
|
|
func TestRequestLogEndpointRejectsUnknownDuplicateAndMalformedQueries(t *testing.T) {
|
|
validCursor := encodeTestCursorPayload(
|
|
`{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-4000-8000-000000000001"}`,
|
|
)
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
}{
|
|
{name: "unknown", query: "unknown=value"},
|
|
{name: "duplicate", query: "limit=1&limit=2"},
|
|
{name: "number", query: "limit=not-a-number"},
|
|
{name: "old time query", query: "from=1784894400000"},
|
|
{name: "ambiguous legacy model", query: "model=client-model"},
|
|
{name: "time", query: "from_ms=not-a-time"},
|
|
{name: "time leading zero", query: "from_ms=01784894400000"},
|
|
{name: "time plus", query: "from_ms=%2B1784894400000"},
|
|
{name: "time negative", query: "from_ms=-1"},
|
|
{name: "time unsafe", query: "from_ms=9007199254740992"},
|
|
{name: "group", query: "group_id=-1"},
|
|
{name: "group leading zero", query: "group_id=01"},
|
|
{name: "group plus", query: "group_id=%2B1"},
|
|
{name: "group whitespace", query: "group_id=%201"},
|
|
{name: "group duplicate", query: "group_id=1&group_id=2"},
|
|
{name: "access key", query: "access_key_id=1.5"},
|
|
{name: "access key leading zero", query: "access_key_id=01"},
|
|
{name: "access key plus", query: "access_key_id=%2B1"},
|
|
{name: "access key whitespace", query: "access_key_id=%201"},
|
|
{name: "access key duplicate", query: "access_key_id=1&access_key_id=2"},
|
|
{name: "legacy upstream key", query: "upstream_key_id=9"},
|
|
{name: "legacy key", query: "key_id=9"},
|
|
{name: "limit leading zero", query: "limit=01"},
|
|
{name: "limit plus", query: "limit=%2B1"},
|
|
{name: "limit whitespace", query: "limit=%201"},
|
|
{name: "request UUID uppercase", query: "request_id=00000000-0000-4000-8000-00000000ABCD"},
|
|
{name: "request UUID version", query: "request_id=00000000-0000-3000-8000-000000000001"},
|
|
{name: "cursor base64", query: "cursor=%25%25%25"},
|
|
{name: "cursor percent encoded newline", query: "cursor=" + validCursor + "%0A"},
|
|
{name: "cursor JSON", query: "cursor=" + encodeTestCursorPayload(`{"v":1`)},
|
|
{name: "cursor unknown field", query: "cursor=" + encodeTestCursorPayload(
|
|
`{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-4000-8000-000000000001","extra":true}`,
|
|
)},
|
|
{name: "cursor old version", query: "cursor=" + encodeTestCursorPayload(
|
|
`{"v":1,"completed_at":"2026-07-24T12:00:00Z","request_id":"00000000-0000-4000-8000-000000000001"}`,
|
|
)},
|
|
{name: "cursor unsafe timestamp", query: "cursor=" + encodeTestCursorPayload(
|
|
`{"v":2,"completed_at_ms":9007199254740992,"request_id":"00000000-0000-4000-8000-000000000001"}`,
|
|
)},
|
|
{name: "cursor UUID", query: "cursor=" + encodeTestCursorPayload(
|
|
`{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-3000-8000-000000000001"}`,
|
|
)},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
reader := &recordingRequestLogReader{}
|
|
engine := newRequestLogTestEngine(t, reader)
|
|
recorder := performRequestLogRequest(engine, "test-auth-key", test.query)
|
|
assertRequestLogErrorCode(t, recorder, "BAD_REQUEST")
|
|
if len(reader.queries) != 0 {
|
|
t.Fatalf("Reader calls = %d, want zero", len(reader.queries))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointRejectsInvalidDomainValues(t *testing.T) {
|
|
equalTime := "1784894400000"
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
}{
|
|
{name: "limit zero", query: "limit=0"},
|
|
{name: "limit above maximum", query: "limit=201"},
|
|
{name: "group zero", query: "group_id=0"},
|
|
{name: "group unsafe", query: "group_id=9007199254740992"},
|
|
{name: "access key zero", query: "access_key_id=0"},
|
|
{name: "access key unsafe", query: "access_key_id=9007199254740992"},
|
|
{name: "unknown channel", query: "channel_id=unknown"},
|
|
{name: "credential zero", query: "credential_id=0"},
|
|
{name: "credential unsafe", query: "credential_id=9007199254740992"},
|
|
{name: "empty client model", query: "client_model="},
|
|
{name: "empty upstream model", query: "upstream_model="},
|
|
{name: "equal range", query: "from_ms=" + equalTime + "&to_ms=" + equalTime},
|
|
{name: "reversed range", query: "from_ms=1784898000000&to_ms=1784894400000"},
|
|
{name: "unknown status", query: "status=unknown"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
reader := &recordingRequestLogReader{}
|
|
engine := newRequestLogTestEngine(t, reader)
|
|
recorder := performRequestLogRequest(engine, "test-auth-key", test.query)
|
|
assertRequestLogErrorCode(t, recorder, "VALIDATION_FAILED")
|
|
if len(reader.queries) != 0 {
|
|
t.Fatalf("Reader calls = %d, want zero", len(reader.queries))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointAcceptsCanonicalNumericBoundaries(t *testing.T) {
|
|
if strconv.IntSize != 64 {
|
|
t.Skip("maximum JavaScript safe integer does not fit uint on this architecture")
|
|
}
|
|
reader := &recordingRequestLogReader{}
|
|
engine := newRequestLogTestEngine(t, reader)
|
|
recorder := performRequestLogRequest(
|
|
engine,
|
|
"test-auth-key",
|
|
"group_id=9007199254740991&access_key_id=9007199254740991&limit=200",
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("response = %d %s, want 200", recorder.Code, recorder.Body.String())
|
|
}
|
|
if len(reader.queries) != 1 || reader.queries[0].GroupID == nil ||
|
|
reader.queries[0].AccessKeyID == nil ||
|
|
uint64(*reader.queries[0].GroupID) != uint64(maxSafeInteger) ||
|
|
uint64(*reader.queries[0].AccessKeyID) != uint64(maxSafeInteger) ||
|
|
reader.queries[0].Limit != maxRequestLogLimit {
|
|
t.Fatalf("List() queries = %#v", reader.queries)
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointParsesAdvancedFilters(t *testing.T) {
|
|
reader := &recordingRequestLogReader{}
|
|
engine := newRequestLogTestEngine(t, reader)
|
|
query := strings.Join([]string{
|
|
"protocol=anthropic",
|
|
"stream=true",
|
|
"final_status_code=529",
|
|
"usage_state=partial",
|
|
"cost_state=priced",
|
|
"pricing_completeness=partial",
|
|
"cache_present=true",
|
|
"channel_id=openai",
|
|
"credential_id=9",
|
|
"attempt_status_code=429",
|
|
"failure_category=rate_limited",
|
|
"error_code=provider_rate_limit",
|
|
"retry_state=retried",
|
|
"retry_count_min=1",
|
|
"retry_count_max=3",
|
|
"first_response_min_ms=10",
|
|
"first_response_max_ms=900",
|
|
"duration_min_ms=20",
|
|
"duration_max_ms=2000",
|
|
"input_tokens_min=100",
|
|
"input_tokens_max=2000",
|
|
"output_tokens_min=10",
|
|
"output_tokens_max=500",
|
|
"cost_min_nano_usd=1000",
|
|
"cost_max_nano_usd=9000000",
|
|
}, "&")
|
|
recorder := performRequestLogRequest(engine, "test-auth-key", query)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("response = %d %s, want 200", recorder.Code, recorder.Body.String())
|
|
}
|
|
if len(reader.queries) != 1 {
|
|
t.Fatalf("Reader calls = %d, want one", len(reader.queries))
|
|
}
|
|
got := reader.queries[0]
|
|
if got.Protocol != protocol.Anthropic || got.Stream == nil || !*got.Stream ||
|
|
got.FinalStatusCode == nil || *got.FinalStatusCode != 529 ||
|
|
got.UsageState != usage.StatePartial || got.CostState != pricing.CostStatePriced ||
|
|
got.PricingCompleteness != pricing.CompletenessPartial ||
|
|
got.CachePresent == nil || !*got.CachePresent ||
|
|
got.ChannelID != channel.OpenAI ||
|
|
got.CredentialID == nil || *got.CredentialID != 9 ||
|
|
got.AttemptStatusCode == nil || *got.AttemptStatusCode != 429 ||
|
|
got.FailureCategory != telemetry.FailureCategoryRateLimited ||
|
|
got.AttemptErrorCode != "provider_rate_limit" || got.RetryState != requestlog.RetryStateRetried ||
|
|
got.RetryCountMin == nil || *got.RetryCountMin != 1 ||
|
|
got.RetryCountMax == nil || *got.RetryCountMax != 3 ||
|
|
got.FirstResponseMinMS == nil || *got.FirstResponseMinMS != 10 ||
|
|
got.FirstResponseMaxMS == nil || *got.FirstResponseMaxMS != 900 ||
|
|
got.DurationMinMS == nil || *got.DurationMinMS != 20 ||
|
|
got.DurationMaxMS == nil || *got.DurationMaxMS != 2000 ||
|
|
got.InputTokensMin == nil || *got.InputTokensMin != 100 ||
|
|
got.InputTokensMax == nil || *got.InputTokensMax != 2000 ||
|
|
got.OutputTokensMin == nil || *got.OutputTokensMin != 10 ||
|
|
got.OutputTokensMax == nil || *got.OutputTokensMax != 500 ||
|
|
got.CostMinNanoUSD == nil || *got.CostMinNanoUSD != 1000 ||
|
|
got.CostMaxNanoUSD == nil || *got.CostMaxNanoUSD != 9000000 {
|
|
t.Fatalf("parsed advanced query = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointRejectsInvalidAdvancedFilters(t *testing.T) {
|
|
tests := []string{
|
|
"protocol=openai",
|
|
"stream=1",
|
|
"final_status_code=1000",
|
|
"usage_state=unknown",
|
|
"cost_state=unknown",
|
|
"pricing_completeness=unknown",
|
|
"cache_present=yes",
|
|
"channel_id=unknown",
|
|
"credential_id=0",
|
|
"attempt_status_code=-1",
|
|
"failure_category=unknown",
|
|
"error_code=",
|
|
"retry_state=unknown",
|
|
"retry_count_min=4&retry_count_max=3",
|
|
"first_response_min_ms=20&first_response_max_ms=10",
|
|
"duration_min_ms=20&duration_max_ms=10",
|
|
"input_tokens_min=20&input_tokens_max=10",
|
|
"output_tokens_min=20&output_tokens_max=10",
|
|
"cost_min_nano_usd=20&cost_max_nano_usd=10",
|
|
}
|
|
for _, query := range tests {
|
|
t.Run(query, func(t *testing.T) {
|
|
reader := &recordingRequestLogReader{}
|
|
recorder := performRequestLogRequest(
|
|
newRequestLogTestEngine(t, reader),
|
|
"test-auth-key",
|
|
query,
|
|
)
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("response = %d %s, want 400", recorder.Code, recorder.Body.String())
|
|
}
|
|
if len(reader.queries) != 0 {
|
|
t.Fatalf("Reader calls = %d, want zero", len(reader.queries))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
|
|
completedAt := time.Date(2026, time.July, 24, 12, 0, 0, 123456789, time.UTC)
|
|
completedAtMS := completedAt.UnixMilli()
|
|
nextCursor := &requestlog.Cursor{
|
|
CompletedAtMS: completedAtMS,
|
|
RequestID: "00000000-0000-4000-8000-000000000502",
|
|
}
|
|
currentName := "Renamed Access Key"
|
|
reader := &recordingRequestLogReader{
|
|
pages: []requestlog.Page{
|
|
{
|
|
Items: []requestlog.Record{{
|
|
RequestID: "00000000-0000-4000-8000-000000000501",
|
|
CompletedAtMS: completedAtMS,
|
|
AccessKey: requestlog.AccessKeyRef{
|
|
ID: 41, Name: ¤tName,
|
|
},
|
|
Protocol: protocol.OpenAICompletions,
|
|
Operation: execution.OperationChatCompletion,
|
|
UpstreamAPI: execution.UpstreamAPIOpenAIChatCompletions,
|
|
ClientModel: "client-model",
|
|
UpstreamModel: "upstream-model",
|
|
UpstreamReportedModel: "reported-model",
|
|
ModelConsistency: telemetry.ModelConsistencyMismatch,
|
|
Status: telemetry.RequestStatusSuccess,
|
|
StatusCode: 200,
|
|
DurationMs: 1234,
|
|
AffinityHit: true,
|
|
GroupID: 12,
|
|
ChannelID: channel.OpenAI,
|
|
CredentialID: 99,
|
|
RouteMode: channel.RouteNative,
|
|
UsageState: usage.StateNotApplicable,
|
|
CostState: pricing.CostStateNotApplicable,
|
|
PricingCompleteness: pricing.CompletenessNotApplicable,
|
|
AttemptCount: 1,
|
|
}},
|
|
NextCursor: nextCursor,
|
|
},
|
|
{Items: []requestlog.Record{}},
|
|
},
|
|
}
|
|
engine := newRequestLogTestEngine(t, reader)
|
|
|
|
query := strings.Join([]string{
|
|
"from_ms=1784890800000",
|
|
"to_ms=1784898000000",
|
|
"group_id=12",
|
|
"client_model=client-model",
|
|
"upstream_model=upstream-model",
|
|
"access_key_id=41",
|
|
"status=success",
|
|
"request_id=00000000-0000-4000-8000-000000000501",
|
|
}, "&")
|
|
recorder := performRequestLogRequest(engine, "test-auth-key", query)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("first response = %d %s", recorder.Code, recorder.Body.String())
|
|
}
|
|
if len(reader.queries) != 1 {
|
|
t.Fatalf("Reader calls = %d, want one", len(reader.queries))
|
|
}
|
|
gotQuery := reader.queries[0]
|
|
if gotQuery.Limit != 50 || gotQuery.FromMS == nil || gotQuery.ToMS == nil ||
|
|
*gotQuery.FromMS != 1784890800000 ||
|
|
*gotQuery.ToMS != 1784898000000 ||
|
|
gotQuery.GroupID == nil || *gotQuery.GroupID != 12 ||
|
|
gotQuery.AccessKeyID == nil || *gotQuery.AccessKeyID != 41 ||
|
|
gotQuery.ClientModel != "client-model" ||
|
|
gotQuery.UpstreamModel != "upstream-model" ||
|
|
gotQuery.Status != telemetry.RequestStatusSuccess ||
|
|
gotQuery.RequestID != "00000000-0000-4000-8000-000000000501" {
|
|
t.Fatalf("parsed ListQuery = %#v", gotQuery)
|
|
}
|
|
|
|
var envelope struct {
|
|
Code int `json:"code"`
|
|
Data struct {
|
|
Items []map[string]any `json:"items"`
|
|
NextCursor *string `json:"next_cursor"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("decode first response: %v", err)
|
|
}
|
|
if envelope.Code != 0 || len(envelope.Data.Items) != 1 ||
|
|
envelope.Data.NextCursor == nil || *envelope.Data.NextCursor == "" {
|
|
t.Fatalf("first response envelope = %#v", envelope)
|
|
}
|
|
if _, exists := envelope.Data.Items[0]["attempts"]; exists {
|
|
t.Fatalf("list item unexpectedly exposes attempts: %#v", envelope.Data.Items[0])
|
|
}
|
|
if envelope.Data.Items[0]["upstream_reported_model"] != "reported-model" ||
|
|
envelope.Data.Items[0]["model_consistency"] != string(telemetry.ModelConsistencyMismatch) ||
|
|
envelope.Data.Items[0]["route_mode"] != string(channel.RouteNative) ||
|
|
envelope.Data.Items[0]["operation"] != string(execution.OperationChatCompletion) ||
|
|
envelope.Data.Items[0]["upstream_api"] != string(execution.UpstreamAPIOpenAIChatCompletions) {
|
|
t.Fatalf("list item model observation = %#v", envelope.Data.Items[0])
|
|
}
|
|
for _, forbidden := range []string{"headers", "body", "url"} {
|
|
if strings.Contains(strings.ToLower(recorder.Body.String()), forbidden) {
|
|
t.Fatalf("response exposes forbidden field %q: %s", forbidden, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
decodedCursor, err := base64.RawURLEncoding.DecodeString(*envelope.Data.NextCursor)
|
|
if err != nil {
|
|
t.Fatalf("next_cursor is not raw URL base64: %v", err)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(decodedCursor, &payload); err != nil {
|
|
t.Fatalf("next_cursor JSON: %v", err)
|
|
}
|
|
if len(payload) != 3 || payload["v"] != float64(2) ||
|
|
payload["completed_at_ms"] != float64(completedAtMS) ||
|
|
payload["request_id"] != nextCursor.RequestID {
|
|
t.Fatalf("next_cursor payload = %#v", payload)
|
|
}
|
|
|
|
second := performRequestLogRequest(
|
|
engine,
|
|
"test-auth-key",
|
|
"cursor="+url.QueryEscape(*envelope.Data.NextCursor),
|
|
)
|
|
if second.Code != http.StatusOK {
|
|
t.Fatalf("second response = %d %s", second.Code, second.Body.String())
|
|
}
|
|
if len(reader.queries) != 2 || reader.queries[1].Cursor == nil ||
|
|
reader.queries[1].Cursor.CompletedAtMS != completedAtMS ||
|
|
reader.queries[1].Cursor.RequestID != nextCursor.RequestID {
|
|
t.Fatalf("decoded cursor query = %#v", reader.queries)
|
|
}
|
|
var emptyEnvelope struct {
|
|
Data struct {
|
|
Items []json.RawMessage `json:"items"`
|
|
NextCursor *string `json:"next_cursor"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(second.Body.Bytes(), &emptyEnvelope); err != nil {
|
|
t.Fatalf("decode second response: %v", err)
|
|
}
|
|
if emptyEnvelope.Data.Items == nil || len(emptyEnvelope.Data.Items) != 0 ||
|
|
emptyEnvelope.Data.NextCursor != nil {
|
|
t.Fatalf("empty response = %#v, want items [] and next_cursor null", emptyEnvelope.Data)
|
|
}
|
|
}
|
|
|
|
func TestRequestLogDetailEndpointReturnsAttemptsAndFrozenPricingReceipt(t *testing.T) {
|
|
requestID := "00000000-0000-4000-8000-000000000611"
|
|
reasoningBudget := int64(4096)
|
|
rate := int64(1_000_000_000)
|
|
amount := int64(1_000_000)
|
|
receipt := &pricing.Receipt{
|
|
SchemaVersion: 3,
|
|
Method: pricing.ReceiptMethodUnitRateSum,
|
|
MethodVersion: 1,
|
|
Currency: "USD",
|
|
Rule: pricing.ReceiptRule{
|
|
ChannelID: string(channel.OpenAI),
|
|
ModelID: "gpt-4.1",
|
|
},
|
|
LineItems: []pricing.ReceiptLine{{
|
|
Code: "input",
|
|
Quantity: 1000,
|
|
RateNanoUSDPerMillion: &rate,
|
|
Multiplier: pricing.Multiplier{Numerator: 1, Denominator: 1},
|
|
State: pricing.ReceiptLinePriced,
|
|
AmountNanoUSD: &amount,
|
|
}},
|
|
TotalNanoUSD: amount,
|
|
}
|
|
reader := &recordingRequestLogReader{details: map[string]requestlog.Record{
|
|
requestID: {
|
|
RequestID: requestID,
|
|
CompletedAtMS: 1_784_894_400_000,
|
|
GroupID: 12,
|
|
ChannelID: channel.OpenAI,
|
|
CredentialID: 99,
|
|
Protocol: protocol.OpenAICompletions,
|
|
Operation: execution.OperationChatCompletion,
|
|
UpstreamAPI: execution.UpstreamAPIOpenAIChatCompletions,
|
|
Status: telemetry.RequestStatusSuccess,
|
|
StatusCode: http.StatusOK,
|
|
AttemptCount: 1,
|
|
UsageState: usage.StateComplete,
|
|
CostState: pricing.CostStatePriced,
|
|
PricingCompleteness: pricing.CompletenessComplete,
|
|
UncachedInputTokens: 1000,
|
|
EstimatedCostNanoUSD: amount,
|
|
Attempts: []requestlog.Attempt{{
|
|
Sequence: 1,
|
|
GroupID: 12,
|
|
GroupName: "Primary",
|
|
ChannelID: channel.OpenAI,
|
|
CredentialID: 99,
|
|
Operation: execution.OperationChatCompletion,
|
|
RouteMode: channel.RouteNative,
|
|
UpstreamModel: "gpt-4.1",
|
|
UpstreamRequestID: "upstream-request-1",
|
|
DispatchState: execution.DispatchMaybeSent,
|
|
ResponseStarted: true,
|
|
UpstreamAPI: execution.UpstreamAPIOpenAIChatCompletions,
|
|
Reasoning: reasoning.Config{
|
|
Effort: "high", BudgetTokens: &reasoningBudget,
|
|
},
|
|
StatusCode: http.StatusOK,
|
|
DurationMs: 1200,
|
|
FailureCategory: telemetry.FailureCategoryOK,
|
|
Action: telemetry.ActionTerminate,
|
|
Committed: true,
|
|
PricingReceipt: receipt,
|
|
}},
|
|
},
|
|
}}
|
|
engine := newRequestLogTestEngine(t, reader)
|
|
request := httptest.NewRequest(http.MethodGet, "/api/logs/"+requestID, nil)
|
|
request.Header.Set("Authorization", "Bearer test-auth-key")
|
|
recorder := httptest.NewRecorder()
|
|
engine.ServeHTTP(recorder, request)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("response = %d %s, want 200", recorder.Code, recorder.Body.String())
|
|
}
|
|
var envelope struct {
|
|
Data struct {
|
|
RequestID string `json:"request_id"`
|
|
Operation string `json:"operation"`
|
|
UpstreamAPI string `json:"upstream_api"`
|
|
ChannelID string `json:"channel_id"`
|
|
CredentialID uint `json:"credential_id"`
|
|
Attempts []struct {
|
|
ChannelID string `json:"channel_id"`
|
|
CredentialID uint `json:"credential_id"`
|
|
Operation string `json:"operation"`
|
|
RouteMode string `json:"route_mode"`
|
|
UpstreamRequestID string `json:"upstream_request_id"`
|
|
DispatchState string `json:"dispatch_state"`
|
|
ResponseStarted bool `json:"response_started"`
|
|
UpstreamAPI string `json:"upstream_api"`
|
|
Reasoning struct {
|
|
Effort string `json:"effort"`
|
|
BudgetTokens string `json:"budget_tokens"`
|
|
} `json:"reasoning"`
|
|
Committed bool `json:"committed"`
|
|
PricingReceipt struct {
|
|
Method string `json:"method"`
|
|
TotalNanoUSD string `json:"total_nano_usd"`
|
|
Rule struct {
|
|
ChannelID string `json:"channel_id"`
|
|
ModelID string `json:"model_id"`
|
|
} `json:"rule"`
|
|
LineItems []struct {
|
|
Quantity string `json:"quantity"`
|
|
AmountNanoUSD string `json:"amount_nano_usd"`
|
|
} `json:"line_items"`
|
|
} `json:"pricing_receipt"`
|
|
} `json:"attempts"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if envelope.Data.RequestID != requestID || envelope.Data.ChannelID != string(channel.OpenAI) ||
|
|
envelope.Data.Operation != string(execution.OperationChatCompletion) ||
|
|
envelope.Data.UpstreamAPI != string(execution.UpstreamAPIOpenAIChatCompletions) ||
|
|
envelope.Data.CredentialID != 99 || len(envelope.Data.Attempts) != 1 ||
|
|
envelope.Data.Attempts[0].ChannelID != string(channel.OpenAI) ||
|
|
envelope.Data.Attempts[0].CredentialID != 99 ||
|
|
envelope.Data.Attempts[0].Operation != string(execution.OperationChatCompletion) ||
|
|
envelope.Data.Attempts[0].RouteMode != string(channel.RouteNative) ||
|
|
envelope.Data.Attempts[0].UpstreamRequestID != "upstream-request-1" ||
|
|
envelope.Data.Attempts[0].DispatchState != string(execution.DispatchMaybeSent) ||
|
|
envelope.Data.Attempts[0].UpstreamAPI != string(execution.UpstreamAPIOpenAIChatCompletions) ||
|
|
envelope.Data.Attempts[0].Reasoning.Effort != "high" ||
|
|
envelope.Data.Attempts[0].Reasoning.BudgetTokens != "4096" ||
|
|
!envelope.Data.Attempts[0].ResponseStarted || !envelope.Data.Attempts[0].Committed ||
|
|
envelope.Data.Attempts[0].PricingReceipt.Method != pricing.ReceiptMethodUnitRateSum ||
|
|
envelope.Data.Attempts[0].PricingReceipt.Rule.ChannelID != string(channel.OpenAI) ||
|
|
envelope.Data.Attempts[0].PricingReceipt.Rule.ModelID != "gpt-4.1" ||
|
|
envelope.Data.Attempts[0].PricingReceipt.TotalNanoUSD != "1000000" ||
|
|
len(envelope.Data.Attempts[0].PricingReceipt.LineItems) != 1 ||
|
|
envelope.Data.Attempts[0].PricingReceipt.LineItems[0].Quantity != "1000" ||
|
|
envelope.Data.Attempts[0].PricingReceipt.LineItems[0].AmountNanoUSD != "1000000" {
|
|
t.Fatalf("detail projection = %#v", envelope.Data)
|
|
}
|
|
for _, legacyField := range []string{`"key_id"`, `"upstream_key_id"`} {
|
|
if strings.Contains(recorder.Body.String(), legacyField) {
|
|
t.Fatalf("detail exposes legacy field %s: %s", legacyField, recorder.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRequestLogPricingReceiptKeepsHistoricalSchemasReadable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
receipt pricing.Receipt
|
|
wantScopeKey string
|
|
wantChannelID string
|
|
}{
|
|
{
|
|
name: "v1 scoped identity",
|
|
receipt: pricing.Receipt{
|
|
SchemaVersion: 1,
|
|
Method: pricing.ReceiptMethodUnitRateSum,
|
|
MethodVersion: 1,
|
|
Currency: "USD",
|
|
Rule: pricing.ReceiptRule{
|
|
ScopeKey: "provider:openai",
|
|
ModelID: "gpt-4.1",
|
|
},
|
|
},
|
|
wantScopeKey: "provider:openai",
|
|
},
|
|
{
|
|
name: "v2 global identity",
|
|
receipt: pricing.Receipt{
|
|
SchemaVersion: 2,
|
|
Method: pricing.ReceiptMethodUnitRateSum,
|
|
MethodVersion: 1,
|
|
Currency: "USD",
|
|
Rule: pricing.ReceiptRule{ModelID: "gpt-4.1"},
|
|
},
|
|
},
|
|
{
|
|
name: "v3 channel identity",
|
|
receipt: pricing.Receipt{
|
|
SchemaVersion: 3,
|
|
Method: pricing.ReceiptMethodUnitRateSum,
|
|
MethodVersion: 1,
|
|
Currency: "USD",
|
|
Rule: pricing.ReceiptRule{
|
|
ChannelID: string(channel.OpenAI),
|
|
ModelID: "gpt-4.1",
|
|
},
|
|
},
|
|
wantChannelID: string(channel.OpenAI),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mapped, err := mapRequestLogPricingReceipt(&test.receipt)
|
|
if err != nil {
|
|
t.Fatalf("mapRequestLogPricingReceipt() error = %v", err)
|
|
}
|
|
gotScopeKey := ""
|
|
if mapped.Rule.ScopeKey != nil {
|
|
gotScopeKey = *mapped.Rule.ScopeKey
|
|
}
|
|
gotChannelID := ""
|
|
if mapped.Rule.ChannelID != nil {
|
|
gotChannelID = *mapped.Rule.ChannelID
|
|
}
|
|
if mapped.SchemaVersion != test.receipt.SchemaVersion ||
|
|
mapped.Rule.ModelID != test.receipt.Rule.ModelID ||
|
|
gotScopeKey != test.wantScopeKey || gotChannelID != test.wantChannelID {
|
|
t.Fatalf("mapped receipt = %#v", mapped)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointProjectsUsageCostAndNullGroupZero(t *testing.T) {
|
|
reasoningBudget := int64(-1)
|
|
reader := &recordingRequestLogReader{pages: []requestlog.Page{{Items: []requestlog.Record{
|
|
{
|
|
RequestID: "00000000-0000-4000-8000-000000000603",
|
|
CompletedAtMS: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
|
Protocol: protocol.OpenAICompletions,
|
|
Status: telemetry.RequestStatusSuccess,
|
|
Reasoning: reasoning.Config{
|
|
Mode: "adaptive",
|
|
Effort: "high",
|
|
BudgetTokens: &reasoningBudget,
|
|
},
|
|
GroupID: 0,
|
|
UsageState: usage.StateComplete,
|
|
CostState: pricing.CostStatePriced,
|
|
PricingCompleteness: pricing.CompletenessComplete,
|
|
UncachedInputTokens: 1,
|
|
CacheReadTokens: 2,
|
|
CacheWrite5MTokens: 3,
|
|
CacheWrite1HTokens: 4,
|
|
CacheWriteUnknownTokens: 6,
|
|
OutputTokens: maxSafeInteger + 1,
|
|
EstimatedCostNanoUSD: 123_456_789_012,
|
|
},
|
|
}}}}
|
|
recorder := performRequestLogRequest(newRequestLogTestEngine(t, reader), "test-auth-key", "")
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("response = %d %s", recorder.Code, recorder.Body.String())
|
|
}
|
|
var envelope struct {
|
|
Data struct {
|
|
Items []struct {
|
|
GroupID *uint `json:"group_id"`
|
|
ChannelID *string `json:"channel_id"`
|
|
CredentialID *uint `json:"credential_id"`
|
|
UsageState string `json:"usage_state"`
|
|
CostState string `json:"cost_state"`
|
|
PricingCompleteness string `json:"pricing_completeness"`
|
|
InputTokens string `json:"input_tokens"`
|
|
CacheReadTokens string `json:"cache_read_tokens"`
|
|
CacheWrite5MTokens string `json:"cache_write_5m_tokens"`
|
|
CacheWrite1HTokens string `json:"cache_write_1h_tokens"`
|
|
CacheWriteUnknownTokens string `json:"cache_write_unknown_tokens"`
|
|
OutputTokens string `json:"output_tokens"`
|
|
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
|
|
Reasoning *struct {
|
|
Mode *string `json:"mode"`
|
|
Effort *string `json:"effort"`
|
|
BudgetTokens *string `json:"budget_tokens"`
|
|
} `json:"reasoning"`
|
|
} `json:"items"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(envelope.Data.Items) != 1 {
|
|
t.Fatalf("items = %#v", envelope.Data.Items)
|
|
}
|
|
item := envelope.Data.Items[0]
|
|
if item.GroupID != nil || item.ChannelID != nil || item.CredentialID != nil ||
|
|
item.UsageState != "complete" || item.CostState != "priced" ||
|
|
item.PricingCompleteness != "complete" ||
|
|
item.InputTokens != "16" || item.CacheReadTokens != "2" ||
|
|
item.CacheWrite5MTokens != "3" || item.CacheWrite1HTokens != "4" ||
|
|
item.CacheWriteUnknownTokens != "6" ||
|
|
item.OutputTokens != "9007199254740992" || item.EstimatedCostNanoUSD != "123456789012" ||
|
|
item.Reasoning == nil || item.Reasoning.Mode == nil || *item.Reasoning.Mode != "adaptive" ||
|
|
item.Reasoning.Effort == nil || *item.Reasoning.Effort != "high" ||
|
|
item.Reasoning.BudgetTokens == nil || *item.Reasoning.BudgetTokens != "-1" {
|
|
t.Fatalf("usage/cost projection = %#v", item)
|
|
}
|
|
}
|
|
|
|
func TestRequestLogResponseUsesNullModelsForProtocolOnlyResponsesResources(t *testing.T) {
|
|
result, err := mapRequestLogListResponse(requestlog.Page{
|
|
Items: []requestlog.Record{{
|
|
RequestID: "00000000-0000-4000-8000-000000000503",
|
|
Protocol: protocol.OpenAIResponses,
|
|
ClientModel: "",
|
|
ModelConsistency: telemetry.ModelConsistencyNotApplicable,
|
|
Status: telemetry.RequestStatusSuccess,
|
|
UsageState: usage.StateNotApplicable,
|
|
CostState: pricing.CostStateNotApplicable,
|
|
PricingCompleteness: pricing.CompletenessNotApplicable,
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("mapRequestLogListResponse() error = %v", err)
|
|
}
|
|
raw, err := json.Marshal(result)
|
|
if err != nil {
|
|
t.Fatalf("marshal response: %v", err)
|
|
}
|
|
body := string(raw)
|
|
for _, field := range []string{
|
|
`"client_model":null`,
|
|
`"upstream_model":null`,
|
|
`"upstream_reported_model":null`,
|
|
`"model_consistency":"not_applicable"`,
|
|
`"reasoning":null`,
|
|
} {
|
|
if !strings.Contains(body, field) {
|
|
t.Fatalf("response = %s, want %s", body, field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRequestLogDetailProjectsEmptyAttemptReasoningAsNull(t *testing.T) {
|
|
result, err := mapRequestLogDetailResponse(requestlog.Record{
|
|
RequestID: "00000000-0000-4000-8000-000000000504",
|
|
Protocol: protocol.OpenAIResponses,
|
|
ModelConsistency: telemetry.ModelConsistencyNotApplicable,
|
|
Status: telemetry.RequestStatusError,
|
|
UsageState: usage.StateNotApplicable,
|
|
CostState: pricing.CostStateNotApplicable,
|
|
PricingCompleteness: pricing.CompletenessNotApplicable,
|
|
Attempts: []requestlog.Attempt{{
|
|
Sequence: 1,
|
|
GroupID: 1,
|
|
GroupName: "group",
|
|
FailureCategory: telemetry.FailureCategoryClientError,
|
|
Action: telemetry.ActionTerminate,
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("mapRequestLogDetailResponse() error = %v", err)
|
|
}
|
|
raw, err := json.Marshal(result)
|
|
if err != nil {
|
|
t.Fatalf("marshal response: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"attempts":[{"sequence":1`) ||
|
|
!strings.Contains(string(raw), `"reasoning":null`) {
|
|
t.Fatalf("response = %s, want null attempt reasoning", raw)
|
|
}
|
|
}
|
|
|
|
func TestRequestLogUsageCostProjectionRejectsUnsafeValues(t *testing.T) {
|
|
base := requestlog.Record{
|
|
UsageState: usage.StateComplete, CostState: pricing.CostStatePriced,
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*requestlog.Record)
|
|
}{
|
|
{name: "usage state", mutate: func(record *requestlog.Record) { record.UsageState = "invalid" }},
|
|
{name: "cost state", mutate: func(record *requestlog.Record) { record.CostState = "invalid" }},
|
|
{name: "negative token", mutate: func(record *requestlog.Record) { record.OutputTokens = -1 }},
|
|
{name: "negative cost", mutate: func(record *requestlog.Record) { record.EstimatedCostNanoUSD = -1 }},
|
|
{
|
|
name: "missing usage cannot be priced",
|
|
mutate: func(record *requestlog.Record) {
|
|
record.UsageState = usage.StateMissing
|
|
},
|
|
},
|
|
{
|
|
name: "not applicable usage cannot be unpriced",
|
|
mutate: func(record *requestlog.Record) {
|
|
record.UsageState = usage.StateNotApplicable
|
|
record.CostState = pricing.CostStateUnpriced
|
|
},
|
|
},
|
|
{
|
|
name: "unpriced cost must be zero",
|
|
mutate: func(record *requestlog.Record) {
|
|
record.CostState = pricing.CostStateUnpriced
|
|
record.EstimatedCostNanoUSD = 1
|
|
},
|
|
},
|
|
}
|
|
if strconv.IntSize == 64 {
|
|
tests = append(tests, struct {
|
|
name string
|
|
mutate func(*requestlog.Record)
|
|
}{
|
|
name: "unsafe final Group ID",
|
|
mutate: func(record *requestlog.Record) {
|
|
unsafeGroupID := uint64(maxSafeInteger) + 1
|
|
record.GroupID = uint(unsafeGroupID)
|
|
},
|
|
})
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
record := base
|
|
test.mutate(&record)
|
|
if _, err := mapRequestLogListResponse(requestlog.Page{Items: []requestlog.Record{record}}); err == nil {
|
|
t.Fatal("mapRequestLogListResponse() error = nil, want rejection")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestLogUsageCostProjectionAcceptsMaximumSafeFinalGroupID(t *testing.T) {
|
|
if strconv.IntSize != 64 {
|
|
t.Skip("maximum JavaScript safe integer does not fit uint on this architecture")
|
|
}
|
|
safeGroupID := uint64(maxSafeInteger)
|
|
record := requestlog.Record{
|
|
GroupID: uint(safeGroupID), UsageState: usage.StateComplete,
|
|
CostState: pricing.CostStatePriced, PricingCompleteness: pricing.CompletenessComplete,
|
|
}
|
|
result, err := mapRequestLogListResponse(requestlog.Page{Items: []requestlog.Record{record}})
|
|
if err != nil {
|
|
t.Fatalf("mapRequestLogListResponse() error = %v", err)
|
|
}
|
|
if len(result.Items) != 1 || result.Items[0].GroupID == nil ||
|
|
uint64(*result.Items[0].GroupID) != safeGroupID {
|
|
t.Fatalf("projected final Group ID = %#v", result.Items)
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointFailsClosedOnCorruptReaderUsageCost(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*requestlog.Record)
|
|
}{
|
|
{
|
|
name: "invalid state matrix",
|
|
mutate: func(record *requestlog.Record) {
|
|
record.UsageState = usage.StateMissing
|
|
record.CostState = pricing.CostStatePriced
|
|
},
|
|
},
|
|
{
|
|
name: "unpriced nonzero cost",
|
|
mutate: func(record *requestlog.Record) {
|
|
record.CostState = pricing.CostStateUnpriced
|
|
record.EstimatedCostNanoUSD = 1
|
|
},
|
|
},
|
|
}
|
|
if strconv.IntSize == 64 {
|
|
tests = append(tests, struct {
|
|
name string
|
|
mutate func(*requestlog.Record)
|
|
}{
|
|
name: "unsafe final Group ID",
|
|
mutate: func(record *requestlog.Record) {
|
|
unsafeGroupID := uint64(maxSafeInteger) + 1
|
|
record.GroupID = uint(unsafeGroupID)
|
|
},
|
|
})
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
record := requestlog.Record{
|
|
RequestID: "00000000-0000-4000-8000-000000000605",
|
|
CompletedAtMS: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
|
UsageState: usage.StateComplete,
|
|
CostState: pricing.CostStatePriced,
|
|
}
|
|
test.mutate(&record)
|
|
reader := &recordingRequestLogReader{
|
|
pages: []requestlog.Page{{Items: []requestlog.Record{record}}},
|
|
}
|
|
recorder := performRequestLogRequest(
|
|
newRequestLogTestEngine(t, reader),
|
|
"test-auth-key",
|
|
"",
|
|
)
|
|
if recorder.Code != http.StatusInternalServerError ||
|
|
!strings.Contains(recorder.Body.String(), "INTERNAL_SERVER_ERROR") ||
|
|
strings.Contains(strings.ToLower(recorder.Body.String()), "unsafe") {
|
|
t.Fatalf("corrupt response = %d %s", recorder.Code, recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointRequiresManagementAuthentication(t *testing.T) {
|
|
reader := &recordingRequestLogReader{}
|
|
engine := newRequestLogTestEngine(t, reader)
|
|
recorder := performRequestLogRequest(engine, "", "")
|
|
if recorder.Code != http.StatusUnauthorized {
|
|
t.Fatalf("response = %d %s, want 401", recorder.Code, recorder.Body.String())
|
|
}
|
|
assertRequestLogErrorCode(t, recorder, "UNAUTHORIZED")
|
|
if len(reader.queries) != 0 {
|
|
t.Fatalf("Reader calls = %d, want zero", len(reader.queries))
|
|
}
|
|
}
|
|
|
|
func TestRequestLogEndpointsBindAccessKeyScopeAndRedactRoutingInternals(t *testing.T) {
|
|
initControlI18n(t)
|
|
fixture := newServiceFixture(t)
|
|
current, err := fixture.service.CreateAccessKey(t.Context(), AccessKeyCreateRequest{
|
|
Name: "log viewer",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateAccessKey(current) error = %v", err)
|
|
}
|
|
other, err := fixture.service.CreateAccessKey(t.Context(), AccessKeyCreateRequest{
|
|
Name: "other private key",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateAccessKey(other) error = %v", err)
|
|
}
|
|
requestID := "00000000-0000-4000-8000-000000000701"
|
|
record := requestlog.Record{
|
|
RequestID: requestID,
|
|
CompletedAtMS: time.Date(2026, time.August, 8, 19, 0, 0, 0, time.UTC).UnixMilli(),
|
|
AccessKey: requestlog.AccessKeyRef{ID: current.ID, Name: ¤t.Name},
|
|
Protocol: protocol.OpenAICompletions,
|
|
Operation: execution.OperationChatCompletion,
|
|
UpstreamAPI: execution.UpstreamAPIAnthropicMessages,
|
|
ClientModel: "client-model",
|
|
UpstreamModel: "private-upstream-model",
|
|
UpstreamReportedModel: "private-reported-model",
|
|
ModelConsistency: telemetry.ModelConsistencyMismatch,
|
|
Status: telemetry.RequestStatusSuccess,
|
|
StatusCode: http.StatusOK,
|
|
DurationMs: 120,
|
|
AttemptCount: 2,
|
|
AffinityHit: true,
|
|
GroupID: 99,
|
|
ChannelID: channel.OpenAI,
|
|
CredentialID: 101,
|
|
RouteMode: channel.RouteConverted,
|
|
UsageState: usage.StateComplete,
|
|
CostState: pricing.CostStatePriced,
|
|
PricingCompleteness: pricing.CompletenessComplete,
|
|
UncachedInputTokens: 10,
|
|
OutputTokens: 2,
|
|
EstimatedCostNanoUSD: 50,
|
|
Attempts: []requestlog.Attempt{{
|
|
Sequence: 1, GroupID: 99, GroupName: "private group",
|
|
ChannelID: channel.OpenAI, CredentialID: 101,
|
|
UpstreamModel: "private-upstream-model",
|
|
}},
|
|
}
|
|
reader := &recordingRequestLogReader{pages: []requestlog.Page{
|
|
{Items: []requestlog.Record{record}},
|
|
{Items: []requestlog.Record{record}},
|
|
{Items: []requestlog.Record{}},
|
|
}}
|
|
fixture.service.requestLogs = reader
|
|
engine := gin.New()
|
|
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
|
|
|
|
list := performRequestLogRequest(engine, current.Key, "client_model=client-model")
|
|
if list.Code != http.StatusOK {
|
|
t.Fatalf("AccessKey logs = %d %s, want 200", list.Code, list.Body.String())
|
|
}
|
|
if len(reader.queries) != 1 || reader.queries[0].AccessKeyID == nil ||
|
|
*reader.queries[0].AccessKeyID != current.ID {
|
|
t.Fatalf("AccessKey list query = %#v", reader.queries)
|
|
}
|
|
assertAccessKeyLogRedaction(t, list.Body.Bytes(), false)
|
|
if strings.Contains(list.Body.String(), other.Name) {
|
|
t.Fatalf("AccessKey logs expose another key: %s", list.Body.String())
|
|
}
|
|
|
|
for _, query := range []string{
|
|
"access_key_id=" + strconv.FormatUint(uint64(other.ID), 10),
|
|
"group_id=99",
|
|
"channel_id=openai",
|
|
"credential_id=101",
|
|
"upstream_model=private-upstream-model",
|
|
"retry_state=retried",
|
|
} {
|
|
recorder := performRequestLogRequest(engine, current.Key, query)
|
|
if recorder.Code != http.StatusBadRequest || len(reader.queries) != 1 {
|
|
t.Fatalf("AccessKey log filter %q = %d %s, calls=%d", query, recorder.Code, recorder.Body.String(), len(reader.queries))
|
|
}
|
|
}
|
|
|
|
detailRequest := httptest.NewRequest(http.MethodGet, "/api/logs/"+requestID, nil)
|
|
detailRequest.Header.Set("Authorization", "Bearer "+current.Key)
|
|
detail := httptest.NewRecorder()
|
|
engine.ServeHTTP(detail, detailRequest)
|
|
if detail.Code != http.StatusOK {
|
|
t.Fatalf("AccessKey log detail = %d %s, want 200", detail.Code, detail.Body.String())
|
|
}
|
|
if len(reader.queries) != 2 || reader.queries[1].RequestID != requestID ||
|
|
reader.queries[1].AccessKeyID == nil || *reader.queries[1].AccessKeyID != current.ID ||
|
|
len(reader.getRequests) != 0 {
|
|
t.Fatalf("AccessKey detail lookup = queries %#v, gets %#v", reader.queries, reader.getRequests)
|
|
}
|
|
assertAccessKeyLogRedaction(t, detail.Body.Bytes(), true)
|
|
|
|
missingRequest := httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/api/logs/00000000-0000-4000-8000-000000000702",
|
|
nil,
|
|
)
|
|
missingRequest.Header.Set("Authorization", "Bearer "+current.Key)
|
|
missing := httptest.NewRecorder()
|
|
engine.ServeHTTP(missing, missingRequest)
|
|
if missing.Code != http.StatusNotFound || len(reader.queries) != 3 ||
|
|
len(reader.getRequests) != 0 {
|
|
t.Fatalf("cross-key detail = %d %s, queries=%#v gets=%#v", missing.Code, missing.Body.String(), reader.queries, reader.getRequests)
|
|
}
|
|
}
|
|
|
|
func assertAccessKeyLogRedaction(t *testing.T, body []byte, detail bool) {
|
|
t.Helper()
|
|
var envelope struct {
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &envelope); err != nil {
|
|
t.Fatalf("decode AccessKey log envelope: %v", err)
|
|
}
|
|
var item map[string]json.RawMessage
|
|
if detail {
|
|
if err := json.Unmarshal(envelope.Data, &item); err != nil {
|
|
t.Fatalf("decode AccessKey log detail: %v", err)
|
|
}
|
|
} else {
|
|
var page struct {
|
|
Items []map[string]json.RawMessage `json:"items"`
|
|
}
|
|
if err := json.Unmarshal(envelope.Data, &page); err != nil || len(page.Items) != 1 {
|
|
t.Fatalf("decode AccessKey log page = %#v/%v", page, err)
|
|
}
|
|
item = page.Items[0]
|
|
}
|
|
for field, want := range map[string]string{
|
|
"upstream_model": "null",
|
|
"upstream_reported_model": "null",
|
|
"model_consistency": `"not_applicable"`,
|
|
"affinity_hit": "false",
|
|
"group_id": "null",
|
|
"channel_id": "null",
|
|
"credential_id": "null",
|
|
"route_mode": "null",
|
|
"upstream_api": "null",
|
|
"attempt_count": "0",
|
|
} {
|
|
if string(item[field]) != want {
|
|
t.Fatalf("AccessKey log %s = %s, want %s; body=%s", field, item[field], want, body)
|
|
}
|
|
}
|
|
if detail && string(item["attempts"]) != "[]" {
|
|
t.Fatalf("AccessKey attempts = %s, want []; body=%s", item["attempts"], body)
|
|
}
|
|
for _, secret := range []string{
|
|
"private-upstream-model", "private-reported-model", "private group",
|
|
} {
|
|
if bytes.Contains(body, []byte(secret)) {
|
|
t.Fatalf("AccessKey log exposes %q: %s", secret, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
type recordingRequestLogReader struct {
|
|
pages []requestlog.Page
|
|
queries []requestlog.ListQuery
|
|
err error
|
|
details map[string]requestlog.Record
|
|
getErr error
|
|
getRequests []string
|
|
}
|
|
|
|
func (reader *recordingRequestLogReader) List(
|
|
_ context.Context,
|
|
query requestlog.ListQuery,
|
|
) (requestlog.Page, error) {
|
|
reader.queries = append(reader.queries, query)
|
|
if reader.err != nil {
|
|
return requestlog.Page{}, reader.err
|
|
}
|
|
index := len(reader.queries) - 1
|
|
if index >= len(reader.pages) {
|
|
return requestlog.Page{Items: []requestlog.Record{}}, nil
|
|
}
|
|
return reader.pages[index], nil
|
|
}
|
|
|
|
func (reader *recordingRequestLogReader) Get(
|
|
_ context.Context,
|
|
requestID string,
|
|
) (requestlog.Record, error) {
|
|
reader.getRequests = append(reader.getRequests, requestID)
|
|
if reader.getErr != nil {
|
|
return requestlog.Record{}, reader.getErr
|
|
}
|
|
if record, ok := reader.details[requestID]; ok {
|
|
return record, nil
|
|
}
|
|
return requestlog.Record{}, fmt.Errorf("request log %s not found", requestID)
|
|
}
|
|
|
|
func newRequestLogTestEngine(t *testing.T, reader RequestLogReader) *gin.Engine {
|
|
t.Helper()
|
|
initControlI18n(t)
|
|
fixture := newServiceFixture(t)
|
|
fixture.service.requestLogs = reader
|
|
engine := gin.New()
|
|
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
|
|
return engine
|
|
}
|
|
|
|
func performRequestLogRequest(engine *gin.Engine, authKey, query string) *httptest.ResponseRecorder {
|
|
target := "/api/logs"
|
|
if query != "" {
|
|
target += "?" + query
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, target, nil)
|
|
if authKey != "" {
|
|
request.Header.Set("Authorization", "Bearer "+authKey)
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
engine.ServeHTTP(recorder, request)
|
|
return recorder
|
|
}
|
|
|
|
func assertRequestLogErrorCode(t *testing.T, recorder *httptest.ResponseRecorder, want string) {
|
|
t.Helper()
|
|
var envelope struct {
|
|
Code string `json:"code"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("decode error response: %v; body=%s", err, recorder.Body.String())
|
|
}
|
|
if recorder.Code != http.StatusBadRequest && want != "UNAUTHORIZED" {
|
|
t.Fatalf("HTTP status = %d, want 400; body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
if envelope.Code != want {
|
|
t.Fatalf("error code = %q, want %q; body=%s", envelope.Code, want, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func encodeTestCursorPayload(payload string) string {
|
|
return base64.RawURLEncoding.EncodeToString([]byte(payload))
|
|
}
|
|
|
|
func TestRequestLogAttemptActionUsesCredentialTerms(t *testing.T) {
|
|
for input, want := range map[telemetry.Action]string{
|
|
telemetry.Action("cooldown_credential"): "cooldown_credential",
|
|
telemetry.Action("fail_credential"): "fail_credential",
|
|
telemetry.ActionTerminate: string(telemetry.ActionTerminate),
|
|
} {
|
|
mapped, err := mapRequestLogAttempt(requestlog.Attempt{Action: input})
|
|
if err != nil {
|
|
t.Fatalf("mapRequestLogAttempt(%q) error = %v", input, err)
|
|
}
|
|
if mapped.Action != want {
|
|
t.Fatalf("mapRequestLogAttempt(%q).Action = %q, want %q", input, mapped.Action, want)
|
|
}
|
|
encoded, err := json.Marshal(mapped)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal(%q) error = %v", input, err)
|
|
}
|
|
if strings.Contains(string(encoded), `"action":"cooldown_key"`) ||
|
|
strings.Contains(string(encoded), `"action":"fail_key"`) {
|
|
t.Fatalf("attempt wire exposes legacy key action: %s", encoded)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Example_requestLogOpaqueCursor() {
|
|
payload := `{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-4000-8000-000000000001"}`
|
|
fmt.Println(encodeTestCursorPayload(payload))
|
|
// Output: eyJ2IjoyLCJjb21wbGV0ZWRfYXRfbXMiOjE3ODQ4OTQ0MDAwMDAsInJlcXVlc3RfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEifQ
|
|
}
|