Commit graph

47287 commits

Author SHA1 Message Date
Mateo Wang
2f63249816 feat(router): add TypeSafe Jev as a complexity router classifier
Backport of #41615 to stable/1.101.x.
Cherry-picked from cf42b607c3 (main).
2026-09-22 23:09:35 +00:00
Tin Chi Lo
3b97c28ab5 feat(auto-router): allow opted-in team members to manage their routers
Prerequisite for #41615 and #41879 on stable/1.101.x.
Cherry-picked from 109ca70f66 (main).
2026-09-22 23:09:31 +00:00
ryan-crabbe-berri
9250fd579c fix(proxy): apply team model aliases on the JWT auth path
Prerequisite for #41615 on stable/1.101.x.
Cherry-picked from 7015bf37bb (main).
2026-09-22 23:09:31 +00:00
mateo-berri
081f65ea44 feat(typesafe): add TypeSafe Jev passthrough with logging and cost tracking
Backport of #41607 to stable/1.101.x.
Cherry-picked from deb9d8aedd (main).

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 21:34:24 +00:00
yuneng-jiang
1fb7e5a9e9
Merge pull request #42332 from BerriAI/litellm_backport_1_101_x_bp_41870
chore(release): backport #41870 to stable/1.101.x
2026-09-21 14:47:16 -07:00
Yuneng Jiang
5b1a903976
chore(deps): bump soupsieve to 2.9.0 2026-09-21 14:18:31 -07:00
Yuneng Jiang
07a3aef0b2
chore(deps): bump anyio to 4.14.2 2026-09-21 14:18:30 -07:00
Mateo Wang
aec3026860
Merge pull request #41870 from BerriAI/litellm_bedrock_openai_gpt_min_max_tokens
fix(bedrock): clamp maxTokens to the 16-token minimum for OpenAI GPT and xAI Grok models on Converse

(cherry picked from commit a6e3a72ed8)
2026-09-21 14:18:30 -07:00
Mateo Wang
8892c21b09
Merge pull request #41704 from BerriAI/litellm_bump_1_101_1
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
LiteLLM Rust / release wheel (push) Has been cancelled
chore(release): bump stable/1.101.x to 1.101.1
2026-09-17 17:21:25 -07:00
mateo-berri
53ee737406 chore(release): bump stable/1.101.x to 1.101.1
Sets the version project-releaser reads for the v1.101.1 stable cut, which carries #41698 (the wildcard license auto_router fix backported from #41684).
2026-09-17 17:00:23 -07:00
Mateo Wang
751fddecae
Merge pull request #41698 from BerriAI/litellm_cherrypick_1_101_x
fix(license): backport #41684 to stable/1.101.x so a wildcard license grants auto_router
2026-09-17 16:46:50 -07:00
mateo-berri
c9ed764c58 fix(license): let a wildcard allowed_features license grant the auto_router feature
Backport of #41684 to stable/1.101.x.
Cherry-picked from c2fbb11dca (litellm_wildcard_license_auto_router).
2026-09-17 16:27:10 -07:00
yuneng-jiang
18243cd7af
Merge pull request #40886 from BerriAI/litellm_backport_redis_chaos_rc_1_101_0
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
LiteLLM Rust / release wheel (push) Has been cancelled
fix(redis): backport Redis chaos fixes and load gate to rc/1.101.0
2026-09-12 14:12:48 -07:00
Yuneng Jiang
abfb2cf329
test(load): backport Redis chaos qualification to rc/1.101.0 (#40482)
(cherry picked from commit 8e4f2abb40)
2026-09-12 12:28:44 -07:00
Yuneng Jiang
c1a834baaf
fix(caching): backport Redis breaker recovery guards to rc/1.101.0 (#40624)
(cherry picked from commit ff4b558243)
2026-09-12 12:28:18 -07:00
Yuneng Jiang
ca4a61304f
fix(redis): backport quiet breaker refusals to rc/1.101.0 (#40620)
(cherry picked from commit ef1a37795c)
2026-09-12 12:27:57 -07:00
Yuneng Jiang
c324a2b3c5
perf(proxy): backport pipelined spend counters to rc/1.101.0 (#40371)
(cherry picked from commit 996ee5635a)
2026-09-12 12:27:18 -07:00
yuneng-jiang
912a3f88f8
Merge pull request #40346 from BerriAI/litellm_backport_40335_rc_1_101_0
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
LiteLLM Rust / release wheel (push) Has been cancelled
fix(otel): backport auth spans and callback merge to rc/1.101.0 (#40335)
2026-09-08 18:31:32 -07:00
Yuneng Jiang
bcddffcbdc
fix(otel): backport auth spans and callback merge to rc/1.101.0
Cherry-pick PR #40335, restoring the Datadog auth span and last-wins callback credential merging.

(cherry picked from commit 43a1b2992a)
2026-09-08 18:28:11 -07:00
yuneng-jiang
f0b6d66b84
Merge pull request #40325 from BerriAI/litellm_backport_37667_rc_1_101_0
feat(team): backport team admin callbacks to rc/1.101.0 (#37667)
2026-09-08 16:42:17 -07:00
yuneng-jiang
f41fc719c1
Merge pull request #40324 from BerriAI/litellm_backport_40312_rc_1_101_0
chore(ui): backport dashboard dependencies to rc/1.101.0 (#40312)
2026-09-08 16:42:09 -07:00
Yuneng Jiang
b6e73cccdb
feat(team): backport team admin callbacks to rc/1.101.0
Backport #37667 without conflict resolution or implementation changes

(cherry picked from commit 90731576e3)
2026-09-08 16:35:28 -07:00
Yuneng Jiang
d8b9fcf399
chore(ui): backport dashboard dependencies to rc/1.101.0
Backport #40312 without conflict resolution or implementation changes

(cherry picked from commit 8280d7ca9d, mainline parent 1)
2026-09-08 16:35:28 -07:00
yuneng-jiang
cbf0c76869
Merge pull request #40321 from BerriAI/litellm_backport_39654_rc_1_101_0
feat(otel): backport tenant trace destinations to rc/1.101.0 (#39654)
2026-09-08 16:34:13 -07:00
Yuneng Jiang
09976043ac
feat(otel): backport tenant trace destinations to rc/1.101.0
Backport #39654 without conflict resolution or implementation changes

(cherry picked from commit 3165951e6d)
2026-09-08 16:29:16 -07:00
yuneng-jiang
744156d297
Merge pull request #40316 from BerriAI/litellm_backport_40203_rc_1_101_0
feat: backport MongoDB sidecar to rc/1.101.0
2026-09-08 15:56:32 -07:00
yuneng-jiang
935bb5260f
feat: backport MongoDB sidecar to rc/1.101.0
(cherry picked from commit b8d573c5f9)
2026-09-08 15:52:58 -07:00
ryan-crabbe-berri
eeb7732fc1
Merge pull request #39994 from BerriAI/litellm_hotfix_mongodb_extra_docker_images
Some checks failed
Unit Tests / caching-local (push) Has been cancelled
Unit Tests / core-utils (push) Has been cancelled
Unit Tests / enterprise-routing (push) Has been cancelled
Unit Tests / integrations (push) Has been cancelled
Unit Tests / All Other Providers (push) Has been cancelled
Unit Tests / Vertex AI (push) Has been cancelled
Unit Tests / misc (push) Has been cancelled
Unit Tests / proxy-auth (push) Has been cancelled
Unit Tests / proxy-infra (push) Has been cancelled
Unit Tests / proxy-server (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Unit Tests: Documentation Validation / documentation (push) Has been cancelled
GitHub Actions Security Analysis / zizmor (push) Has been cancelled
Unit Tests / enterprise-package (push) Has been cancelled
Unit Tests / proxy-endpoints (push) Has been cancelled
Unit Tests / proxy-extras (push) Has been cancelled
Unit Tests / responses-caching-types (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
fix(docker): ship pymongo in the proxy images for the MongoDB vector store
2026-09-05 18:12:56 -07:00
Yuneng Jiang
16fd14f537
fix(docker): ship pymongo in the proxy images for the MongoDB vector store
The MongoDB Atlas vector store provider imports pymongo lazily from the
opt-in `mongodb` extra, but none of the shipped images installed that
extra. Any image-based deployment that configured a MongoDB vector store
failed at search time with "requires the 'pymongo' package", which the
user cannot fix without extending the image

Adds `--extra mongodb` to every uv sync in the root Dockerfile,
Dockerfile.database, Dockerfile.non_root, and the gateway component
image. The backend component does not serve /vector_stores so it is left
as is. The extra resolves from the existing uv.lock to pymongo 4.17.0
plus dnspython 2.8.0, no lock change needed
2026-09-05 18:03:24 -07:00
yuneng-jiang
e56b26c255
Merge pull request #39849 from BerriAI/litellm_internal_staging
chore(ci): promote internal staging to main
2026-09-05 16:41:13 -07:00
yuneng-jiang
bde58f9b41
Merge pull request #39959 from BerriAI/litellm_/release-ui-build-e256a3
chore: rebuild Admin UI bundle for the next release
2026-09-05 16:40:13 -07:00
Yuneng Jiang
0e118342ac
chore: update Next.js build artifacts (2026-09-05 23:25 UTC, node v24.19.0) 2026-09-05 16:25:57 -07:00
yuneng-jiang
b1e2f5bc0b
Merge pull request #39969 from BerriAI/revert-39121-litellm_lazy_sdk_import
revert: perf: lazy-load SDK symbols so import litellm stays under 60 MB RSS (#39121)
2026-09-05 16:22:55 -07:00
devin-ai-integration[bot]
9a5564f00e
fix(ui): read Usage Total Requests tile from gateway request counts (#39963) 2026-09-05 16:20:56 -07:00
Yuneng Jiang
eee162b317
fix(lazy_imports): type import_map as Mapping to stay under the LIT001 budget
The revert restored the dict annotation that #39121 had loosened to Mapping,
and the LIT001 ceiling has been ratcheted down since, so the gate rejected the
one reintroduced hit. Annotation only, no behavior change.
2026-09-05 16:12:45 -07:00
ryan-crabbe-berri
587311d0e4
Merge pull request #39968 from BerriAI/litellm_remove_dead_model_hub_public_dialog
fix(ui): remove unreachable AI Hub dialog that put the session key in a URL
2026-09-05 16:10:15 -07:00
yuneng-jiang
45cf1a7ef1
Revert "perf: lazy-load SDK symbols so import litellm stays under 60 MB RSS (…"
This reverts commit c091dd4608.
2026-09-05 16:07:09 -07:00
ryan-crabbe-berri
79a5614c18 fix(ui): remove unreachable AI Hub dialog that put the session key in a URL
The Public Model Hub dialog in ModelHubTable was never opened (its open setter had no callers), but its See Page button navigated to /model_hub_table?key=<session key>. Delete the dialog, its state, the handler and the unused router import so the path cannot be revived.
2026-09-05 16:04:50 -07:00
yuneng-jiang
1b25132863
Merge pull request #39953 from BerriAI/litellm_/litellm-e2e-flaky-test-2159ae
test(e2e): judge /v1/messages streaming on the clock, not on the provider's delta count
2026-09-05 16:04:45 -07:00
ryan-crabbe-berri
a0c9095cfc
Merge pull request #39960 from BerriAI/litellm_cache_hit_guardrail_spend
fix(proxy): keep guardrail cost in spend on cache hits
2026-09-05 15:53:34 -07:00
ryan-crabbe-berri
63156a7bd6 test(proxy): explain the proxy_server patches in the cache-hit regression test
The test-quality gate counts every patch of a litellm internal against a ceiling, and the three patches this test needs pushed it over. The callback imports increment_spend_counters, update_cache and proxy_logging_obj from proxy_server inside its own body, so there is no seam to inject fakes through; every other test in this file uses the same three patches for the same reason

Claude-Session: https://claude.ai/code/session_01EX13mWex6RaBo9PYnkAtFW
2026-09-05 15:44:37 -07:00
ryan-crabbe-berri
acddd21860 fix(proxy): keep guardrail cost in spend on cache hits
The proxy cost callback zeroed response_cost whenever cache_hit was true. That rule dates from Jan 2024 when it was the only place cache hits were priced. The logging layer has priced the LLM share at 0 on a cache hit since Aug 2024, and since guardrail cost joined the standard logging payload the proxy-side zeroing has thrown away a real provider charge: a pre_call guardrail runs before the cache is consulted, so a cached response still cost whatever the guardrail billed. Drop the redundant zeroing so the payload's response_cost, which is already LLM 0 + guardrail cost, reaches spend logs, daily tables and budgets untouched

Claude-Session: https://claude.ai/code/session_01EX13mWex6RaBo9PYnkAtFW
2026-09-05 15:32:02 -07:00
Mateo Wang
bf51dea36b
Merge pull request #39862 from BerriAI/litellm_lit_6992_cohere_parse
feat(ocr): add Cohere Parse support for cohere and azure_ai
2026-09-05 15:16:16 -07:00
yuneng-jiang
6a4fb2bbe8
Merge pull request #39938 from BerriAI/litellm_e2e_vertex_cache_first_call
test(e2e): prove Vertex context caching on the first cold call and on the spend row
2026-09-05 15:10:15 -07:00
Yuneng Jiang
cd976624d1
test(e2e): drop the explanatory sentence from the StreamingResponse docstring 2026-09-05 14:53:12 -07:00
Yuneng Jiang
b55a4317a6
test(e2e): annotate new stream-timing locals as Final and trim the docstrings 2026-09-05 14:49:56 -07:00
Yassin Kortam
0cb759772c
fix(ui): show indirectly granted and name-keyed MCP servers in the tool matrix (#35154)
* fix(ui): show indirectly granted and name-keyed MCP servers in the tool matrix

The MCP tool permission editor was fed the direct server list only, so a server a
principal reaches through an access group or a toolset never appeared in the matrix.
That single blind spot produced two opposite bugs depending on how a save handler
filtered mcp_tool_permissions: filtering by the selected servers deletes an indirect
server's allowlist, and because a missing entry means "no restriction from this
level", the principal silently gains every tool on it; not filtering leaves a stale
entry that keeps a removed access group's server reachable, since a server named
under mcp_tool_permissions is entitled on purpose.

The editor now resolves the selected access groups and toolsets to their servers and
renders them alongside the direct ones, badged with where the grant comes from, so an
admin can see and clear an inherited server's tools like any other. Resolution reuses
the data the selector already loads: access groups resolve from each server's
mcp_access_groups, toolsets from the toolset's own tool list. When that data cannot be
loaded the editor says so instead of rendering an empty list, because an absent
inherited server reads as "there are none". Servers named only by an
mcp_tool_permissions key are listed too, which is what makes a leftover entry
visible; the opt-out sentinel still renders nothing, since it short-circuits the
backend resolver to zero servers.

Opening the editor no longer applies the delete-blocked-by-default allowlist to an
inherited server. Writing an entry for one would narrow a grant the admin never
touched just by opening the form; direct servers keep that default.

Both components also matched on server_id alone, while the backend accepts a server
id, name or alias interchangeably. A grant or allowlist written by API or config with
a name rendered as a selected server with no tools under it, which reads as "this
server has no tools". Matching now covers all three identifiers, and an edit writes
back to the key the entry already uses rather than forking a second id-keyed entry.

The same mismatch could also put one server under several keys at once, its id and
its name for instance. The backend unions every key's list, so reading one key
understated what was in force and writing one key left the others granting. The
resolver now reports, per server, the key an edit keeps, the equivalent keys it
supersedes, and the union those keys allow; the card renders the union and every
write goes through one function that writes the kept key and drops the superseded
ones. A key that also names a DIFFERENT server, which happens when two servers share
a name, is never dropped, because dropping it would strip the neighbouring server's
restriction; the card names such a key and says its tools stay allowed until the
servers no longer share the name, so an admin is told rather than left to infer it
from an edit that bounces back.

A third divergence from the backend sat in the same matching. The backend resolves an
identifier with exact-id precedence: a string that is a registry server id names that
server and stops, and only a string that is no server's id falls back to name and alias,
which can name several. Matching all three fields at once meant a server merely named
after another server's id joined the matrix as if it had been selected, and because it
landed there as a directly selected server it also received the delete-blocked default
write on open. Since an mcp_tool_permissions key is itself a grant source, saving then
handed out a server nobody granted, with no admin gesture involved. Identifier
resolution now mirrors the backend's precedence, and a key is read as this server's only
when it resolves back to it, so an entry that belongs to the id's owner is neither read
into this server's allowlist nor overwritten by an edit made against it.

A toolset grant was also invisible to the tool matrix. The backend unions a toolset's
tools with whatever mcp_tool_permissions allows, so a toolset-only grant restricts the
server to that toolset's tools; the editor read the map alone, found no entry and
rendered every tool on the server as allowed. Deselecting one from that state wrote all
the others as a permission entry, and the union turned a revocation into a grant of
every tool the toolset never included. The resolved entry now carries the toolset's
tools, so the matrix opens on what is actually in force, the delete-blocked default is
withheld from a server a toolset restricts, and a write keeps out the tools only the
toolset accounts for so a grant that ends with the toolset does not become a standing
one. Those tools cannot be revoked from this screen at all, since the backend unions
them in; they render allowed and locked and the card says which of them a toolset holds
open and where to go to revoke them.

That guard originally covered only the keys an edit supersedes, on the assumption that
the key it keeps names one server. It does not when a shared key is a server's only
entry: it then becomes the key an edit writes, and writing it moves the other server's
allowlist too, which is the widening the guard exists to prevent. The key an edit writes
is now the first one naming this server and no other, falling back to the server's own
id, so a shared key is never written through and an edit against one card cannot reach
the server behind the other. Both cards say the shared key holds tools open, since
neither can revoke them.

No owner's save handler changes here. With the full effective set now available to
the editor, the key and team handlers can filter against it instead of guessing,
which makes the internal-user surface's unfiltered save redundant

Resolves LIT-4963
Resolves LIT-4958

* chore: drop tsbuildinfo churn from merge

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): satisfy dashboard lint budgets

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep MCP tool allowlists for indirect grants

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep standing MCP grants on team save

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(ui): format TeamInfo and hoist inline object args

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep MCP tool allowlists for team servers granted indirectly (#35153)

* fix(ui): filter team MCP tool allowlists against the effective server set

Saving a team filtered mcp_tool_permissions down to the directly selected servers. A server reached
through an access group or a toolset is never in that list, so any save dropped its entry, including
a save that only changed the team alias. Because the resolver unions tool-permission keys into the
entitled server set and treats a missing entry as "no restriction from this level", the team kept
the server and lost the tool allowlist on it

Filtering on the direct list alone cannot get this right in either direction. Keeping every entry a
level did not directly select leaves a removed access group's server reachable through its own stale
entry, which breaks revocation. Dropping on deselection alone widens a server that an access group
still supplies

The save handler now resolves the effective server set with resolveEffectiveMcpServers and keeps an
entry only when something other than the entry itself still grants that server: a direct selection, a
selected access group, or a selected toolset. Unified access group ids are added when that selection
is untouched, since the loaded server list is then still accurate

When the server or toolset list cannot be resolved, every entry is kept and the admin is told the
allowlists were saved unchanged. Pruning on incomplete knowledge is the direction that silently
widens, so it only happens when the editor can show the server became unreachable. A failed lookup
and a changed access group selection are separate cases in a tagged union, so the notice names what
actually happened instead of describing the intentional one as a failure, and both hooks gate the
filter symmetrically so a save fired before toolsets settle cannot resolve against an empty toolset
list

Resolves LIT-4961

* fix(ui): resolve team MCP grants from access group metadata and refuse unsafe saves

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): resolve team access group grants from team info when the access group list is role-gated

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): match every selected access group by id instead of by count

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): reload team access group grants at save time

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep frontend lint budget within limit

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): cover a standing allowlist no group grant covers at load or save

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(ui): keep MCP grant inputs in named variables for the lint budget

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): guard MCP default write on toolset load, keep create toolsets, fix flat view

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-05 14:49:30 -07:00
Yuneng Jiang
d56affa814
test(e2e): judge /v1/messages streaming on the clock, not on the provider's delta count
The Anthropic and Together AI /v1/messages streaming tests required at
least two content_block_delta events. How many deltas a reply is split into
is the provider's choice, and Haiku answers a short count in one or two, so
the assertion failed on provider variance with no change in the proxy: four
of the day's full runs on the PR e2e gate went red on it on 2026-09-05.

The harness now stamps when each SSE event reached the client
(StreamingResponse.stream_event_arrivals, index-aligned with stream_events,
with the clock injectable so the reader has a unit test). Both tests ask for
a reply long enough to take seconds to generate and require the first
content delta to land at least STREAM_MIN_LEAD_SECONDS before message_stop.
A relayed stream shows a lead of about two seconds. A proxy that buffered
the response delivers every event in one burst and fails every time, which
a whole-response buffering relay in front of a live proxy confirmed. The
event-grammar assertions are unchanged.

Replay hands the proxy its recorded chunks back to back, so timing says
nothing there. The assertion is gated on provider_paces_stream() and replay
proves the grammar only, which tests/e2e/CLAUDE.md now says.
2026-09-05 14:44:21 -07:00
ryan-crabbe-berri
1745d74293
Merge pull request #39853 from BerriAI/litellm_guardrail_usage_cost_ui
feat(ui): show guardrail usage units and cost on the Guardrails Monitor
2026-09-05 14:41:11 -07:00
ryan-crabbe-berri
b99d8ac38e refactor(ui): keep guardrail usage code under the inline-object-arg lint budget
The staging merge pushed local/no-large-inline-object-arg to 567 against a 554 ceiling, and 15 of those hits came from this branch. useGuardrailsUsageDetail now takes the guardrail id positionally with the date window as its second argument, the usageUnits tests build CounterMath rows through a positional helper, and the overview fixture spreads a base row inside the array instead of calling a factory

Claude-Session: https://claude.ai/code/session_01EX13mWex6RaBo9PYnkAtFW
2026-09-05 14:27:08 -07:00