fix(coderd/x/chatd): fix compaction still over limit check (#26377)

Addresses
[CODAGT-620](https://linear.app/codercom/issue/CODAGT-620/session-can-get-stuck-at-compaction-with-request-failed).
We have logic that checks whether message compaction still leaves the
chat over the context limit. We want to abort if it does - if we didn't,
we'd get into an endless compaction loop. The check's logic was faulty.
This PR changes fixes it. The new flow is:

1. In iteration 1, a chat runner commits a message compaction summary.
2. In iteration 2, the runner submits the newly compacted conversation
to the LLM provider in order to generate the next message.
3. In iteration 3, 4, 5, etc., if the conversation needs compaction, the
runner looks up the configured context limit and the first assistant
message after the last compaction summary. It compares the context usage
on that message with the context limit. If the usage is over the limit,
it returns an error.
This commit is contained in:
Hugo Dutka
2026-06-15 17:17:50 +02:00
committed by GitHub
parent b61b62f4b3
commit 3cde346cbb
5 changed files with 290 additions and 52 deletions
+44 -3
View File
@@ -5738,11 +5738,12 @@ func TestActiveServer_Compaction(t *testing.T) {
requireTextPart(t, messages[len(messages)-1], "done without compaction")
})
t.Run("fails when compaction leaves chat over limit", func(t *testing.T) {
t.Run("next message fails when compaction continuation stayed over limit", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
db, ps := dbtestutil.NewDB(t)
logSink := testutil.NewFakeSink(t)
var streamCount atomic.Int32
anthropicURL := chattest.NewAnthropic(t, func(req *chattest.AnthropicRequest) chattest.AnthropicResponse {
body := anthropicRequestBody(t, *req)
@@ -5771,7 +5772,10 @@ func TestActiveServer_Compaction(t *testing.T) {
Return(workspacesdk.ReadFileLinesResponse{Success: true, FileSize: 12, TotalLines: 1, LinesRead: 1, Content: "1 package main"}, nil).
Times(1)
reg := prometheus.NewRegistry()
server := newActiveTestServer(t, db, ps, func(cfg *chatd.Config) {
cfg.Logger = logSink.Logger()
cfg.PrometheusRegistry = reg
cfg.AgentConn = func(_ context.Context, agentID uuid.UUID) (workspacesdk.AgentConn, func(), error) {
require.Equal(t, dbAgent.ID, agentID)
return mockConn, func() {}, nil
@@ -5783,15 +5787,52 @@ func TestActiveServer_Compaction(t *testing.T) {
APIKeyID: testAPIKeyID(t, db, user.ID),
WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true},
AgentID: uuid.NullUUID{UUID: dbAgent.ID, Valid: true},
Title: "compaction-still-over-limit",
Title: "compaction-next-message-over-limit",
ModelConfigID: model.ID,
InitialUserContent: []codersdk.ChatMessagePart{
codersdk.ChatMessageText("read the file and stay too large"),
},
})
require.NoError(t, err)
chat = waitForChatStatus(ctx, t, db, chat.ID, database.ChatStatusWaiting)
require.False(t, chat.LastError.Valid)
require.Equal(t, int32(2), streamCount.Load())
messages := chatMessages(ctx, t, db, chat.ID)
requireTextPart(t, messages[len(messages)-1], "still too large")
_, err = server.SendMessage(ctx, chatd.SendMessageOptions{
ChatID: chat.ID,
CreatedBy: user.ID,
APIKeyID: testAPIKeyID(t, db, user.ID),
ModelConfigID: model.ID,
Content: []codersdk.ChatMessagePart{
codersdk.ChatMessageText("continue after the large compacted turn"),
},
})
require.NoError(t, err)
chat = waitForChatStatus(ctx, t, db, chat.ID, database.ChatStatusError)
require.Contains(t, chatLastErrorMessage(chat.LastError), "The chat request failed unexpectedly.")
require.Equal(t,
"Conversation compaction could not reduce the history below the configured limit. Raise the compaction limit in settings, or start a new conversation.",
chatLastErrorMessage(chat.LastError),
)
require.Equal(t, int32(2), streamCount.Load(), "over-limit history should fail before another model stream")
requireChatdMetricCounter(t, reg, "coderd_chatd_compaction_total", 1, map[string]string{
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"result": "error",
})
isCompactionFailureLog := func(e slog.SinkEntry) bool {
if e.Level != slog.LevelWarn || e.Message != "chat generation failed" {
return false
}
errValue, ok := sinkFieldValue(e.Fields, "error")
return ok && strings.Contains(fmt.Sprintf("%v", errValue), "compaction left the chat above the compaction limit")
}
testutil.Eventually(ctx, t, func(context.Context) bool {
return len(logSink.Entries(isCompactionFailureLog)) > 0
}, testutil.IntervalFast)
})
}
+47 -37
View File
@@ -129,21 +129,19 @@ const (
generationFinishReasonMaxSteps generationFinishReason = "max_steps"
)
type compactionTrigger string
const (
compactionTriggerRequired compactionTrigger = "required"
compactionTriggerAlreadyCompacted compactionTrigger = "already_compacted"
var errCompactionStillOverLimit = chaterror.WithClassification(
xerrors.New("compaction left the chat above the compaction limit"),
chaterror.ClassifiedError{
Message: "Conversation compaction could not reduce the history below the configured limit. Raise the compaction limit in settings, or start a new conversation.",
Kind: codersdk.ChatErrorKindConfig,
},
)
var errCompactionStillOverLimit = xerrors.New("compaction left the chat above the compaction limit")
type generationDecision struct {
kind generationActionKind
localToolCalls []fantasy.ToolCallContent
pendingDynamicToolCalls []pendingDynamicToolCall
finishReason generationFinishReason
compactionTrigger compactionTrigger
promotedMessageID int64
}
@@ -181,15 +179,17 @@ func isTerminalGeneration(err error) bool {
}
type generationDecisionInput struct {
chat database.Chat
messages []database.ChatMessage
dynamicToolNames map[string]bool
exclusiveToolNames map[string]bool
stopAfterTools map[string]struct{}
maxSteps int
compactionEnabled bool
compactionNeeded bool
workspaceContextEligible bool
chat database.Chat
messages []database.ChatMessage
dynamicToolNames map[string]bool
exclusiveToolNames map[string]bool
stopAfterTools map[string]struct{}
maxSteps int
compactionEnabled bool
compactionNeeded bool
compactionThresholdPercent int32
compactionContextLimit int64
workspaceContextEligible bool
}
// shouldPersistWorkspaceContext reports whether the committed chat
@@ -265,11 +265,11 @@ func decideGenerationAction(input generationDecisionInput) (generationDecision,
if input.compactionEnabled && input.compactionNeeded {
compactionRequirement = compactionRequirementNeeded
}
switch compactionStatusFromHistory(input.messages, compactionRequirement) {
switch compactionStatusFromHistory(input.messages, compactionRequirement, input.compactionThresholdPercent, input.compactionContextLimit) {
case compactionStatusNeeded:
return generationDecision{kind: generationActionCompact, compactionTrigger: compactionTriggerRequired}, nil
return generationDecision{kind: generationActionCompact}, nil
case compactionStatusAfterCompaction:
return generationDecision{kind: generationActionGenerateAssistant, compactionTrigger: compactionTriggerAlreadyCompacted}, nil
return generationDecision{kind: generationActionGenerateAssistant}, nil
case compactionStatusStillOverLimit:
return generationDecision{}, terminalGeneration(errCompactionStillOverLimit)
case compactionStatusNotNeeded:
@@ -279,6 +279,13 @@ func decideGenerationAction(input generationDecisionInput) (generationDecision,
}
}
func generationCompactionThreshold(compaction *generationCompaction) int32 {
if compaction == nil {
return 0
}
return compaction.Options.ThresholdPercent
}
func unresolvedToolCallsFromHistory(
messages []database.ChatMessage,
dynamicToolNames map[string]bool,
@@ -361,15 +368,17 @@ func (s *taskStarter) StartGeneration(ctx context.Context, input chatWorkerTaskS
cleanup := prepared.Cleanup
decision, err := retryGenerationPhase(ctx, s.waitGenerationPhaseBackoff, func() (generationDecision, error) {
return decideGenerationAction(generationDecisionInput{
chat: prepared.Chat,
messages: prepared.Messages,
dynamicToolNames: prepared.DynamicToolNames,
exclusiveToolNames: prepared.ExclusiveToolNames,
stopAfterTools: prepared.StopAfterTools,
maxSteps: prepared.MaxSteps,
compactionEnabled: prepared.Compaction != nil,
compactionNeeded: prepared.Compaction != nil && prepared.Compaction.Required,
workspaceContextEligible: prepared.WorkspaceContextEligible,
chat: prepared.Chat,
messages: prepared.Messages,
dynamicToolNames: prepared.DynamicToolNames,
exclusiveToolNames: prepared.ExclusiveToolNames,
stopAfterTools: prepared.StopAfterTools,
maxSteps: prepared.MaxSteps,
compactionEnabled: prepared.Compaction != nil,
compactionNeeded: prepared.Compaction != nil && prepared.Compaction.Required,
compactionThresholdPercent: generationCompactionThreshold(prepared.Compaction),
compactionContextLimit: prepared.ContextLimitFallback,
workspaceContextEligible: prepared.WorkspaceContextEligible,
})
})
if err != nil {
@@ -377,6 +386,14 @@ func (s *taskStarter) StartGeneration(ctx context.Context, input chatWorkerTaskS
if errors.Is(err, errTaskExpectedExit) {
return errTaskExpectedExit
}
if errors.Is(err, errCompactionStillOverLimit) && prepared.Compaction != nil {
s.server.metrics.RecordCompaction(
compactionProvider(prepared.Compaction.Options),
compactionModel(prepared.Compaction.Options),
false,
errCompactionStillOverLimit,
)
}
return s.finishGenerationError(ctx, machine, input, 0, err, generationAttemptNotRequired)
}
@@ -389,7 +406,7 @@ func (s *taskStarter) StartGeneration(ctx context.Context, input chatWorkerTaskS
cleanup()
return s.finishGenerationTurn(ctx, machine, input, 0, decision, generationAttemptNotRequired)
case generationActionGenerateAssistant:
actionErr = s.generateAssistant(ctx, machine, input, prepared, decision)
actionErr = s.generateAssistant(ctx, machine, input, prepared)
case generationActionExecuteLocalTools:
actionErr = s.executeLocalTools(ctx, machine, input, prepared, decision)
case generationActionCompact:
@@ -600,7 +617,6 @@ func (s *taskStarter) generateAssistant(
machine *chatstate.ChatMachine,
input chatWorkerTaskStartInput,
prepared generationPrepared,
decision generationDecision,
) error {
attempt, _, publish, closeEpisode, err := s.beginGenerationAttempt(ctx, machine, input)
if err != nil {
@@ -625,12 +641,6 @@ func (s *taskStarter) generateAssistant(
if err != nil {
return err
}
if decision.compactionTrigger == compactionTriggerAlreadyCompacted &&
shouldCompactPromptUsage(outcome.Step.Usage, prepared.ContextLimitFallback, prepared.Compaction.Options.ThresholdPercent) {
err := errCompactionStillOverLimit
s.server.metrics.RecordCompaction(compactionProvider(prepared.Compaction.Options), compactionModel(prepared.Compaction.Options), false, err)
return s.finishGenerationError(ctx, machine, input, attempt, err, generationAttemptRequired)
}
if len(outcome.Step.Content) == 0 {
return s.finishGenerationTurn(ctx, machine, input, attempt, generationDecision{kind: generationActionFinishTurn, finishReason: generationFinishReasonComplete}, generationAttemptRequired)
}
+1 -8
View File
@@ -609,14 +609,7 @@ func (server *Server) prepareGeneration(
func latestPromptUsage(messages []database.ChatMessage) fantasy.Usage {
for i := len(messages) - 1; i >= 0; i-- {
usage := fantasy.Usage{
InputTokens: messages[i].InputTokens.Int64,
OutputTokens: messages[i].OutputTokens.Int64,
TotalTokens: messages[i].TotalTokens.Int64,
ReasoningTokens: messages[i].ReasoningTokens.Int64,
CacheCreationTokens: messages[i].CacheCreationTokens.Int64,
CacheReadTokens: messages[i].CacheReadTokens.Int64,
}
usage := usageFromMessage(messages[i])
if usage != (fantasy.Usage{}) {
return usage
}
+55 -4
View File
@@ -363,18 +363,32 @@ const (
compactionRequirementNeeded
)
func compactionStatusFromHistory(messages []database.ChatMessage, requirement compactionRequirement) compactionStatus {
func compactionStatusFromHistory(
messages []database.ChatMessage,
requirement compactionRequirement,
thresholdPercent int32,
contextLimit int64,
) compactionStatus {
boundaryIndex := latestCompactionBoundaryIndex(messages)
if requirement == compactionRequirementNeeded {
if boundaryIndex == -1 {
return compactionStatusNeeded
}
if hasUncompressedAssistantAfter(messages, boundaryIndex) {
// The first assistant response after the previously compacted summary.
// Messages with role ChatMessageRoleAssistant carry context usage.
// Looking at ChatMessageRoleAssistant is enough - ChatMessageRoleTool
// does not carry context usage, and is always preceded by an assistant
// message.
if assistant, ok := firstUncompressedAssistantAfter(messages, boundaryIndex); ok &&
postCompactionAssistantOverLimit(assistant, thresholdPercent, contextLimit) {
return compactionStatusStillOverLimit
}
if hasUncompressedMessageAfter(messages, boundaryIndex) {
return compactionStatusNeeded
}
return compactionStatusAfterCompaction
}
if boundaryIndex != -1 && !hasUncompressedAssistantAfter(messages, boundaryIndex) {
if boundaryIndex != -1 && !hasUncompressedMessageAfter(messages, boundaryIndex) {
return compactionStatusAfterCompaction
}
return compactionStatusNotNeeded
@@ -406,19 +420,56 @@ func isCompactionBoundaryMessage(msg database.ChatMessage) bool {
return false
}
func hasUncompressedAssistantAfter(messages []database.ChatMessage, index int) bool {
func firstUncompressedAssistantAfter(messages []database.ChatMessage, index int) (database.ChatMessage, bool) {
for i := index + 1; i < len(messages); i++ {
msg := messages[i]
if msg.Deleted || msg.Compressed {
continue
}
if msg.Role == database.ChatMessageRoleAssistant {
return msg, true
}
}
return database.ChatMessage{}, false
}
func hasUncompressedMessageAfter(messages []database.ChatMessage, index int) bool {
for i := index + 1; i < len(messages); i++ {
msg := messages[i]
if !msg.Deleted && !msg.Compressed {
return true
}
}
return false
}
func postCompactionAssistantOverLimit(msg database.ChatMessage, thresholdPercent int32, contextLimit int64) bool {
return shouldCompactPromptUsage(usageFromMessage(msg), contextLimit, thresholdPercent)
}
func usageFromMessage(msg database.ChatMessage) fantasy.Usage {
var usage fantasy.Usage
if msg.InputTokens.Valid {
usage.InputTokens = msg.InputTokens.Int64
}
if msg.OutputTokens.Valid {
usage.OutputTokens = msg.OutputTokens.Int64
}
if msg.TotalTokens.Valid {
usage.TotalTokens = msg.TotalTokens.Int64
}
if msg.ReasoningTokens.Valid {
usage.ReasoningTokens = msg.ReasoningTokens.Int64
}
if msg.CacheCreationTokens.Valid {
usage.CacheCreationTokens = msg.CacheCreationTokens.Int64
}
if msg.CacheReadTokens.Valid {
usage.CacheReadTokens = msg.CacheReadTokens.Int64
}
return usage
}
func historyHasStopAfterToolResult(messages []database.ChatMessage, stopAfterTools map[string]struct{}) (bool, error) {
if len(stopAfterTools) == 0 {
return false, nil
+143
View File
@@ -272,6 +272,134 @@ func TestCurrentTurnStepCount_CountsAssistantMessagesAfterLatestUser(t *testing.
require.Equal(t, 2, got)
}
func TestDecisionCompactsAgainAfterPostCompactionTurn(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, false, codersdk.ChatMessageText("initial request")),
dbMessage(t, 2, database.ChatMessageRoleUser, true, codersdk.ChatMessageText("compacted summary")),
dbMessage(t, 3, database.ChatMessageRoleAssistant, true, codersdk.ChatMessageToolCall("summary-1", "chat_summarized", nil)),
dbMessage(t, 4, database.ChatMessageRoleTool, true, codersdk.ChatMessageToolResult("summary-1", "chat_summarized", json.RawMessage(`{}`), false, false)),
dbMessage(t, 5, database.ChatMessageRoleAssistant, false, codersdk.ChatMessageText("continued after compaction")),
dbMessage(t, 6, database.ChatMessageRoleUser, false, codersdk.ChatMessageText("next request")),
}
decision, err := decideGenerationAction(generationDecisionInput{
messages: messages,
compactionEnabled: true,
compactionNeeded: true,
compactionThresholdPercent: 70,
compactionContextLimit: 100,
})
require.NoError(t, err)
require.Equal(t, generationActionCompact, decision.kind)
}
func TestCompactionStatusFromHistory(t *testing.T) {
t.Parallel()
const thresholdPercent = int32(70)
t.Run("needed without boundary", func(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, false, codersdk.ChatMessageText("start")),
dbMessage(t, 2, database.ChatMessageRoleAssistant, false, codersdk.ChatMessageToolCall("read-1", "read_file", json.RawMessage(`{}`))),
}
got := compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 100)
require.Equal(t, compactionStatusNeeded, got)
})
t.Run("after compaction without post boundary history", func(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, true, codersdk.ChatMessageText("summary")),
dbMessage(t, 2, database.ChatMessageRoleAssistant, true, codersdk.ChatMessageToolCall("summary-1", "chat_summarized", nil)),
dbMessage(t, 3, database.ChatMessageRoleTool, true, codersdk.ChatMessageToolResult("summary-1", "chat_summarized", json.RawMessage(`{}`), false, false)),
}
got := compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 100)
require.Equal(t, compactionStatusAfterCompaction, got)
})
t.Run("needed after under limit post compaction assistant", func(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, true, codersdk.ChatMessageText("summary")),
dbMessage(t, 2, database.ChatMessageRoleAssistant, true, codersdk.ChatMessageToolCall("summary-1", "chat_summarized", nil)),
dbMessage(t, 3, database.ChatMessageRoleTool, true, codersdk.ChatMessageToolResult("summary-1", "chat_summarized", json.RawMessage(`{}`), false, false)),
withUsage(dbMessage(t, 4, database.ChatMessageRoleAssistant, false, codersdk.ChatMessageText("continued")), 20, 100),
dbMessage(t, 5, database.ChatMessageRoleUser, false, codersdk.ChatMessageText("next")),
}
got := compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 100)
require.Equal(t, compactionStatusNeeded, got)
})
t.Run("still over limit from first post compaction assistant usage", func(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, true, codersdk.ChatMessageText("summary")),
dbMessage(t, 2, database.ChatMessageRoleAssistant, true, codersdk.ChatMessageToolCall("summary-1", "chat_summarized", nil)),
dbMessage(t, 3, database.ChatMessageRoleTool, true, codersdk.ChatMessageToolResult("summary-1", "chat_summarized", json.RawMessage(`{}`), false, false)),
withUsage(dbMessage(t, 4, database.ChatMessageRoleAssistant, false, codersdk.ChatMessageToolCall("read-1", "read_file", json.RawMessage(`{}`))), 80, 100),
}
got := compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 100)
require.Equal(t, compactionStatusStillOverLimit, got)
})
t.Run("still over limit includes prompt cache tokens", func(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, true, codersdk.ChatMessageText("summary")),
dbMessage(t, 2, database.ChatMessageRoleAssistant, true, codersdk.ChatMessageToolCall("summary-1", "chat_summarized", nil)),
dbMessage(t, 3, database.ChatMessageRoleTool, true, codersdk.ChatMessageToolResult("summary-1", "chat_summarized", json.RawMessage(`{}`), false, false)),
withUsageTokens(dbMessage(t, 4, database.ChatMessageRoleAssistant, false, codersdk.ChatMessageToolCall("read-1", "read_file", json.RawMessage(`{}`))), fantasy.Usage{CacheReadTokens: 80}, 100),
}
got := compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 100)
require.Equal(t, compactionStatusStillOverLimit, got)
})
t.Run("still over limit uses configured context limit", func(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, true, codersdk.ChatMessageText("summary")),
dbMessage(t, 2, database.ChatMessageRoleAssistant, true, codersdk.ChatMessageToolCall("summary-1", "chat_summarized", nil)),
dbMessage(t, 3, database.ChatMessageRoleTool, true, codersdk.ChatMessageToolResult("summary-1", "chat_summarized", json.RawMessage(`{}`), false, false)),
withUsage(dbMessage(t, 4, database.ChatMessageRoleAssistant, false, codersdk.ChatMessageToolCall("read-1", "read_file", json.RawMessage(`{}`))), 80, 200),
}
got := compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 100)
require.Equal(t, compactionStatusStillOverLimit, got)
got = compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 200)
require.Equal(t, compactionStatusNeeded, got)
})
t.Run("still over limit includes exact threshold boundary", func(t *testing.T) {
t.Parallel()
messages := []database.ChatMessage{
dbMessage(t, 1, database.ChatMessageRoleUser, true, codersdk.ChatMessageText("summary")),
dbMessage(t, 2, database.ChatMessageRoleAssistant, true, codersdk.ChatMessageToolCall("summary-1", "chat_summarized", nil)),
dbMessage(t, 3, database.ChatMessageRoleTool, true, codersdk.ChatMessageToolResult("summary-1", "chat_summarized", json.RawMessage(`{}`), false, false)),
withUsage(dbMessage(t, 4, database.ChatMessageRoleAssistant, false, codersdk.ChatMessageToolCall("read-1", "read_file", json.RawMessage(`{}`))), 70, 100),
}
got := compactionStatusFromHistory(messages, compactionRequirementNeeded, thresholdPercent, 100)
require.Equal(t, compactionStatusStillOverLimit, got)
})
}
func TestDecisionDetectsStopAfterToolFromCommittedHistory(t *testing.T) {
t.Parallel()
@@ -494,6 +622,21 @@ func dbMessage(t *testing.T, id int64, role database.ChatMessageRole, compressed
}
}
func withUsage(msg database.ChatMessage, inputTokens int64, contextLimit int64) database.ChatMessage {
return withUsageTokens(msg, fantasy.Usage{InputTokens: inputTokens, TotalTokens: inputTokens}, contextLimit)
}
func withUsageTokens(msg database.ChatMessage, usage fantasy.Usage, contextLimit int64) database.ChatMessage {
msg.InputTokens = sql.NullInt64{Int64: usage.InputTokens, Valid: usage.InputTokens != 0}
msg.OutputTokens = sql.NullInt64{Int64: usage.OutputTokens, Valid: usage.OutputTokens != 0}
msg.TotalTokens = sql.NullInt64{Int64: usage.TotalTokens, Valid: usage.TotalTokens != 0}
msg.ReasoningTokens = sql.NullInt64{Int64: usage.ReasoningTokens, Valid: usage.ReasoningTokens != 0}
msg.CacheCreationTokens = sql.NullInt64{Int64: usage.CacheCreationTokens, Valid: usage.CacheCreationTokens != 0}
msg.CacheReadTokens = sql.NullInt64{Int64: usage.CacheReadTokens, Valid: usage.CacheReadTokens != 0}
msg.ContextLimit = sql.NullInt64{Int64: contextLimit, Valid: contextLimit != 0}
return msg
}
func requireNotNilTime(t *testing.T, value *time.Time) time.Time {
t.Helper()
require.NotNil(t, value)