mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
* feat(proxy): add uncapped server-side team usage export route GET /team/daily/activity/export answers the same scoping as /team/daily/activity/aggregated with one unbounded rollup query, so keys past USAGE_TOP_API_KEYS_LIMIT are included. Supports daily, daily_with_keys, daily_with_users and daily_with_models export types as CSV (default) or JSON. The PTU flat-cost sentinel stays in the plain daily rollup and is excluded from the keyed and per-model exports Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(ui): export team usage server-side when the key list was truncated When the aggregated spend response reports api_key truncation, EntityUsage passes a serverExport into the export modal that downloads CSV or JSON from GET /team/daily/activity/export instead of building the file from the truncated on-screen data. apiClient gains a responseType option so the download can arrive as a Blob, and truncation no longer blocks the export button Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): cover team usage export types, sentinel handling and scope Unit tests pin the uncapped key rollup past USAGE_TOP_API_KEYS_LIMIT, PTU sentinel inclusion in the daily rollup and exclusion elsewhere, the per-user fold, and the CSV column layout. Integration tests exercise the route against a live proxy, including member scope denial Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(proxy): tidy team usage export route Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): use membership test for export type branch (PLR1714) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(ui): format exportBlockedReason test with prettier Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): satisfy type-discipline gate in team usage export Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): pass export rows as a sequence to the response model Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy-behavior): cover team usage export in the daily activity scope matrix Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(proxy): carry PTU flat cost and escape formulas in team usage export Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): keep the truncation export block on surfaces without a server export Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore(ui): drop redundant comments in team export call and modal test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): audit cells for team usage export Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): tighten team usage export audit cells Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): type the export params tuple and fold user keys in one pass Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(ui): bring entity usage export helpers under eslint budgets Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(ui): prettier-format UsagePageView after merge Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { getApiKeyTruncation, getExportBlockedReason, type UsageFetchState } from "./exportBlockedReason";
|
|
|
|
const state = (overrides: Partial<UsageFetchState> = {}): UsageFetchState => ({
|
|
coversRange: true,
|
|
cancelled: false,
|
|
failed: false,
|
|
apiKeyTruncation: undefined,
|
|
...overrides,
|
|
});
|
|
|
|
describe("getExportBlockedReason", () => {
|
|
it("lets the export through once the data on screen covers the range", () => {
|
|
expect(getExportBlockedReason(state())).toBeUndefined();
|
|
});
|
|
|
|
it("blocks whenever the data on screen does not cover the range, which is when a CSV silently under-reports", () => {
|
|
expect(getExportBlockedReason(state({ coversRange: false }))).toMatch(/still loading/i);
|
|
});
|
|
|
|
it("blocks after a stopped fetch and says a reload is what fixes it", () => {
|
|
const reason = getExportBlockedReason(state({ coversRange: false, cancelled: true }));
|
|
|
|
expect(reason).toMatch(/stopped/i);
|
|
expect(reason).toMatch(/reload/i);
|
|
});
|
|
|
|
it("blocks after a failed page and names the failure rather than the stop", () => {
|
|
const reason = getExportBlockedReason(state({ coversRange: false, failed: true, cancelled: true }));
|
|
|
|
expect(reason).toMatch(/failed to load/i);
|
|
expect(reason).not.toMatch(/stopped/i);
|
|
});
|
|
|
|
it("blocks when the aggregated endpoint dropped keys, since a per-team CSV would miss them", () => {
|
|
const reason = getExportBlockedReason(state({ apiKeyTruncation: { limit: 100, total: 3000 } }));
|
|
|
|
expect(reason).toMatch(/100 highest-spend keys of 3000/);
|
|
expect(reason).toMatch(/USAGE_TOP_API_KEYS_LIMIT/);
|
|
});
|
|
|
|
it("does not block on truncation when a server export will cover every key", () => {
|
|
expect(getExportBlockedReason(state({ apiKeyTruncation: null }))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("getApiKeyTruncation", () => {
|
|
it("reports truncation once the proxy saw more keys than it returned", () => {
|
|
expect(getApiKeyTruncation(100, 101)).toEqual({ limit: 100, total: 101 });
|
|
});
|
|
|
|
it("stays quiet when exactly the cap exists, since every key is on screen", () => {
|
|
expect(getApiKeyTruncation(100, 100)).toBeUndefined();
|
|
expect(getApiKeyTruncation(100, 7)).toBeUndefined();
|
|
});
|
|
|
|
it("stays quiet when the response carries no cap, as the paginated fallback does", () => {
|
|
expect(getApiKeyTruncation(undefined, undefined)).toBeUndefined();
|
|
expect(getApiKeyTruncation(100, null)).toBeUndefined();
|
|
});
|
|
});
|