Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ jobs:
run: |
go test ./internal/spend -run '^$' -fuzz '^FuzzReaderBoundedFormats$' -fuzztime 1000x -parallel 2 -timeout 2m
go test ./internal/spend -run '^$' -fuzz '^FuzzSessionBudget$' -fuzztime 1000x -parallel 2 -timeout 2m
go test ./internal/spend -run '^$' -fuzz '^FuzzDailyCounts$' -fuzztime 10000x -parallel 2 -timeout 2m
go test ./internal/compare -run '^$' -fuzz '^FuzzCanonicalSummary$' -fuzztime 10000x -parallel 2 -timeout 2m
go test ./internal/compare -run '^$' -fuzz '^FuzzEmbeddedSketch$' -fuzztime 10000x -parallel 2 -timeout 2m
go test ./internal/compare -run '^$' -fuzz '^FuzzScanSummary$' -fuzztime 10000x -parallel 2 -timeout 2m
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ jobs:
run: go test ./internal/cli -run '^TestReleaseBinary' -count=1 -v
- name: Pinned SQL export through the packaged importer
if: matrix.os == 'linux' && matrix.arch == 'amd64'
timeout-minutes: 5
timeout-minutes: 10
env:
FLEETDIFF_BIN: ${{ github.workspace }}/unpacked/fleetdiff
FLEETDIFF_SPEND_DOCKER: '1'
Expand Down
107 changes: 107 additions & 0 deletions docs/ROW_COVERAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
<!-- Code authors: Vijay and Codex -->

# Are All Requests In The Spend Log?

**8 exported rows versus 9 daily requests: 1 missing row.** This synthetic
fixture reproduces a real response-ID collision through pinned LiteLLM's proxy
and database writers. The next UTC day matches: 1 row and 1 daily request.
The check leaves token and spend totals exactly as recorded in the export.

This optional check is unreleased. Build this branch to try it:

```sh
go build -o bin/fleetdiff ./cmd/fleetdiff
```

## Export And Compare

Use the [read-only SQL recipe](../examples/litellm-spend/export.sql) with
read-only access to both `LiteLLM_SpendLogs` and `LiteLLM_DailyUserSpend`.
Configure the `litellm_readonly` PostgreSQL service through your existing
credential mechanism. The [coverage export helper](../examples/litellm-spend/export-coverage.sh)
creates a new private directory and exports both files in one repeatable-read,
read-only transaction:

```sh
umask 077
export_dir="$HOME/litellm-coverage-export"
sh examples/litellm-spend/export-coverage.sh "$export_dir" \
2026-09-23T00:00:00Z 2026-10-09T00:00:00Z
bin/fleetdiff investigate --litellm-spend "$export_dir/spend.csv" \
--litellm-daily-spend "$export_dir/daily.csv" --group-by key
```

Replace the historical bounds with your interval of complete UTC days. The
16-day export leaves room for the default two complete seven-day periods.
Use `--before-period` and `--after-period` for explicit periods. Session review
also accepts the option with `--period` or `--session-budget-usd`.

The daily projection sums `api_requests`, `successful_requests` and
`failed_requests` across all daily user/model/key rows for each date. Its CSV
contains only those three counts and `date`; no identities or content. The
request export is unchanged. The helper refuses an existing directory and uses
distinct inherited file descriptors so filename aliases cannot overwrite an
export. Its `daily_csv_file` SQL variable is internal to the helper, not a
general output-path option. Use the existing `export.sql` commands for a single
request export. Omit `--litellm-daily-spend` to keep the existing report byte for byte.

## Reading Coverage

Run the recorded fixture from the repository root:

```sh
bin/fleetdiff investigate \
--litellm-spend examples/litellm-spend/coverage_runtime_spend.csv \
--litellm-daily-spend examples/litellm-spend/coverage_runtime_daily.csv \
--before-period 2026-10-07 --after-period 2026-10-08
```

The report begins:

```text
Spend-log row coverage (relative to LiteLLM daily request counts):
2026-10-07 UTC: 8 exported rows; 9 daily requests (8 successful, 1 failed); 1 missing row.
2026-10-08 UTC: 1 exported row; 1 daily request (1 successful, 0 failed); counts match.
Token and spend totals below use exported rows only.
```

All selected request rows count here, including failed records and call types
excluded from token analysis. UTC days follow request `startTime`, not receipt
or export time. A positive difference is reported as missing rows relative to
the daily request count. Extra rows are a discrepancy to review, never a
negative missing count. No token, request, spend or ranking total is imputed.

JSON adds `row_coverage.days`: `exported_rows`, `api_requests`,
`successful_requests`, `failed_requests`, `missing_rows` and `status`, with
`date_utc`. An absent daily row has null daily counts and unknown coverage.
Partial UTC days have null `missing_rows`; full-day counters cannot establish
coverage for a selected part of that day. A real exported zero remains zero.
Empty request exports can still be checked with explicit periods; the usage
comparison then says it has no analyzable requests.

## Limits

- Match application, date bounds, filters and retention in both inputs. Use one
LiteLLM database scope and allow its asynchronous writers to settle before
exporting completed days. A shared database snapshot aligns reads, not the
completion times of separate writes.
- The daily table is a second accounting path, not an independent request
oracle. Logging configuration, retention and lost writes can affect either
path. Matching counts cannot reveal requests absent from both, establish
complete token usage, or certify a provider invoice. A shortfall identifies
records to investigate, not their cause.
- In the pinned runtime, disabling failure logging (`disable_error_logs=true`)
omitted the failed request from **both** tables. Successful response-cache
hits counted in both despite making no provider call. These are logged
spend-transaction counts, not universal ingress or provider-send counts.
- The projection and runtime definitions are pinned to LiteLLM **v1.104.0** in
[images.json](../examples/litellm-spend/images.json). Recheck other versions,
logging configurations and routes before interpreting their counts. This
check compares request records, not provider sends or retry attempts.
- Daily inputs are capped at 3,660 dates, 1 MiB and 512 bytes per CSV line.
Duplicate dates, malformed counts and extra columns fail with source-safe
errors. Dates with no daily row remain unknown rather than being filled in.

See [verification](../examples/litellm-spend/TESTING.md) and the
[request-level accounting contract](../examples/litellm-spend/README.md#data-and-limits).
5 changes: 4 additions & 1 deletion examples/litellm-spend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
increase, and two keys account for all of it.** The report separates request
count from request size where the usage records support that comparison.

Install [fleetdiff v0.6.0](../../README.md#2-install-v060), then run this fixture
Install [fleetdiff v0.8.0](../../README.md#2-install-v080), then run this fixture
from the repository root with the verified binary:

```sh
Expand Down Expand Up @@ -79,6 +79,9 @@ Use `--group-by key|team|user|end-user|session`; model breakdowns are automatic.
set your own strong secret in an environment variable and pass
`--hash-secret-env NAME`. Add `--show-hashes` to display them.

To check for rows missing relative to LiteLLM's daily counters, use the optional
[daily row coverage check](../../docs/ROW_COVERAGE.md) (unreleased).

### Usage Details (Default On)

The SQL includes these three projection aliases by default. Add
Expand Down
37 changes: 37 additions & 0 deletions examples/litellm-spend/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,43 @@ manually; do not prune unrelated Docker resources.

## Coverage

### Daily Row Reconciliation

`test_row_coverage_runtime.py` runs the actual pinned proxy, a loopback mock
provider, and both asynchronous PostgreSQL writers against the image's full
Prisma schema. PostgreSQL has `--network none`; the proxy, mock provider and
client share only that container's loopback namespace. No paid provider,
external network, source-only inference or directly inserted spend/daily row
is used. Request clock references are controlled to test midnight; persistence
and accounting code are unchanged. Error logging is tested in its default,
explicitly enabled and disabled configurations.

| Runtime case | SpendLogs row increment | api_requests | successful_requests | failed_requests |
| --- | ---: | ---: | ---: | ---: |
| Successful non-streaming request | 1 | 1 | 1 | 0 |
| Successful streaming request | 1 | 1 | 1 | 0 |
| Response-cache hit (no provider forward) | 1 | 1 | 1 | 0 |
| Failed request, default/enabled error logging | 1 | 1 | 0 | 1 |
| Failed request, error logging disabled | 0 | 0 | 0 | 0 |
| Two forwards with the same provider response ID, combined | 1 | 2 | 2 | 0 |
| Request with no user identity | 1 | 1 | 1 | 0 |

A request starting before midnight and ending afterward increments the start
day's UTC counters. The second collision request returns HTTP 200, reaches the
provider and increments the daily table while its duplicate SpendLogs primary
key loses a row. The [recorded fixture](coverage_runtime_expected.json) binds
the observations and synthetic exports to image, source, SQL and harness hashes.
`coverage_runtime_fixture.py` checks the exported fixture; `test_row_coverage.py`
checks both CLI modes, unchanged accounting and source-safe output. The optional
daily SQL export uses a SELECT-only role and the same read-only snapshot as the
request export. Omitting the CLI option preserves the v0.8.0 output goldens.

See [row coverage and its limits](../../docs/ROW_COVERAGE.md). These runtime
cases pin one chat-completions profile with retries disabled, not every endpoint
or deployment configuration.

### Request-Level Accounting

- The original 20-row fixture has identical CSV, JSON, and JSONL records.
Request, token, zero-only, and failure arithmetic is checked separately
from the actual importer output.
Expand Down
3 changes: 3 additions & 0 deletions examples/litellm-spend/coverage_runtime_daily.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
date,api_requests,successful_requests,failed_requests
2026-10-07,9,8,1
2026-10-08,1,1,0
3 changes: 3 additions & 0 deletions examples/litellm-spend/coverage_runtime_daily.csv.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2026 Vijay and Codex
Fresh synthetic counters from the same pinned offline runtime database, exported by export-coverage.sh.
Loading
Loading