Skip to content

Diagnostic LLM APIs

Files pile up against a storage ceiling nothing reports

The batch pipeline has run every night for two years and tonight it stops on the first step, uploading the input file, with an error nobody has seen before. Nothing was deployed. The key works, because the same key just listed a thousand files without complaint. Reads are fine. Retrieval is fine. Inference is fine. The only thing that is broken is writing, and it is broken because a number you have never once looked at reached a ceiling you have never once been shown.

Read-only key Python and Node.js Tests included
Warehouse storage racks
Photo by Petr on Unsplash
The short answer

Add it up yourself, because nothing will add it up for you. There is no endpoint on either provider that reports a storage quota, a consumed figure or a remaining one. The listing is the only surface that knows anything about your files, so the script pages through all of it and sums the size field: bytes on OpenAI's file object, size_bytes on Anthropic's.

The ceilings are different numbers on different containers and that matters. OpenAI documents 2.5 TB per project, 512 MB per file, and says in as many words that there is no organization-wide storage limit. Anthropic documents 1 TB per organization and 500 MB per file, and returns HTTP 400 with a storage-limit message when you cross it. So on one provider you audit per project and on the other per organization, and a number that looks safe under one rule can be over the line under the other.

Then grade four things, because a file store fills up in four ways. The total, as a share of the documented ceiling. The concentration, from grouping OpenAI's files by purpose, because one class is almost always most of the bytes and that is the class to sweep. The outliers, individual files near the per-file cap, which is a second and entirely separate ceiling. And the expiry coverage: files with no expires_at older than your retention window, which is the population that can only ever grow.

Keep the measured half and the documented half apart on every line. The total is measured. The ceiling is a number out of a docs page that no request can confirm, and it can be renegotiated for one account or republished for everyone without anything the script reads changing at all. Printing a percentage without saying which half was measured is how an audit becomes a rumour.

The repair is a deletion and the script never performs one. Deleted files cannot be recovered, the metadata gives you no way to tell a dead file from a load-bearing one, and on OpenAI deleting a file also removes it from every vector store it is attached to.

The problem in plain words

Uploading a file is the easy half of a two-part transaction whose second half nobody wrote. Batch input files, batch output and error files, fine-tuning corpora, fine-tune result files, images for vision, documents for retrieval: every one of them is created by a pipeline that had a reason to create it, and none of them is deleted by anything, because at the moment the pipeline finishes nobody has decided the file is finished. It might be needed for a re-run. It might be needed for an audit. It costs nothing visible to keep.

So the store grows monotonically, and it grows in a place with no gauge on it. There is no dashboard number, no header, no field on any response that says how full it is. OpenAI's listing endpoint defaults to returning ten thousand objects per page and never once suggests that this is a lot. The first feedback the platform gives you is a hard failure, and it arrives on the write path, which means it arrives inside a production pipeline rather than inside an audit.

That asymmetry is the whole reason this is worth a scheduled script. Everything that reads keeps working perfectly at 100% of quota. Retrieval works, inference works, listing works, your monitoring is green. Only the next upload fails, and it fails at whatever hour your batch job happens to run.

The second ceiling makes it worse by being invisible in the total. A per-file size cap applies independently of the store-wide limit, so a single oversized upload fails on a store that is 3% full, and the error looks close enough to the quota error that the first hour of the investigation goes into the wrong number. An audit that only reports a total will confidently tell you there is plenty of room while the actual failure is one file that is too big on its own.

A file perrequestuploaded,referenced onceNo expiry setthe default isforeverCeiling reachedand never reportedbeforeUploads startto 400reads are allstill fineFound on thewrite pathin production, midpipeline
Every step here succeeded. Uploading is easy, deleting was never anybody's ticket, and the ceiling is only visible from inside it.

Why it happens

The ceiling is not readable and the script has to be honest about that. The total is measured: it is the sum of a field, over every page, and it is exactly as right as the pagination. The limit is a documented constant that came out of a docs page and cannot be confirmed by any request. Those are different kinds of fact, they age differently, and a percentage computed from one of each inherits the weaker one. So the output labels them, the limit is a command-line argument with a documented default rather than a hardcoded truth, and a run against a changed platform reports a wrong percentage loudly instead of a right-looking one quietly.

The two providers put the ceiling on different containers. OpenAI's 2.5 TB is per project and the docs state there is no organization-wide limit at all, so ten projects are ten separate ceilings and a project key sees exactly one of them. Anthropic's 1 TB is per organization, and the Files API is workspace-scoped, so summing one workspace tells you a fraction of the number that actually governs you. The script prints which container it just measured, and names the stores it did not look at, because an unaudited project is not an empty one.

Pagination is the finding, not a detail of it. An audit that reads the first page and reports a total is not wrong by a little. On a store big enough to matter it is wrong by an order of magnitude and it is wrong in the reassuring direction. So the script pages to the end on both providers, using each one's own cursor — after on OpenAI, next_page handed back as page on Anthropic — and prints how many pages it read. If it stops on a page limit it says the number is a floor and not a total, because a floor is still useful and a total that is secretly a floor is not.

Grouping by purpose is what turns a number into an action. "You are at 71% of quota" is not something anybody can act on. "You are at 71%, and 64 points of that is batch_output from jobs that finished more than 90 days ago" is a sweep somebody can write this afternoon. The classes are not equally deletable, either, and one of them already deletes itself: OpenAI documents that files with purpose=batch expire after 30 days by default while everything else persists until manually deleted. A store dominated by batch will drain on its own. A store dominated by batch_output will not.

Two ceilings, and only one of them is a total. The per-file cap is not a fraction of the quota, it is its own limit, and a file approaching it fails an upload the store-level number says is fine. The script grades them separately and reports both, because the two errors look similar enough in production that the wrong one gets investigated first.

This is a ceiling, not a cost curve, and the difference is which note you are reading. There is a published note about vector store bytes: a stock billed by the hour, trended over ninety days against retrieval volume, priced from a gibibyte_hours line item. Its finding is money and its shape is a slope. This one reads a different object — Anthropic states that Files API operations are free — and its finding is not on the invoice at all. It is how many uploads you have left before a pipeline stops. Both are about bytes nobody deleted, and that is the whole of the resemblance.

The fix, as a flow

Neither provider will tell you how full the file store is. There is no endpoint that returns a quota, a remaining figure or a percentage, so the only way to know is to walk every page of the listing and add the bytes up yourself. That is the whole script. What makes it worth running before the ceiling rather than after is that the ceiling arrives on an upload: reads keep working, the audit keeps passing, and the first thing that fails is the pipeline writing the next file.

Every page, summed by purposeno endpoint reports thisOver the warning shareuploads are what will failOne purpose dominatesthe class worth sweeping firstA file near the per file capa second ceiling entirelyNothing carries an expiryso the total only growsWell under, and ageing outa store with a lifecycle
The outcome is a quantity rather than a status. Two ceilings apply at once, and only one of them is about the total.

How to fix it

Give it whichever credentials you have, and know what each one sees

OPENAI_API_KEY, a project key set to Read Only, reads GET /v1/files for one project, which is the container the 2.5 TB applies to. ANTHROPIC_API_KEY reads the Anthropic listing for one workspace, which is a slice of the organization the 1 TB applies to. Set either, or both. The script audits what it was given and says which stores it did not look at.

Page to the end and count the pages

OpenAI pages on after, carrying the last id of each page forward, and its list response marks has_more as a required field. Anthropic pages by handing the response's next_page back as the page parameter until it comes back null. --max-pages stops a runaway, and stopping there changes the wording of the result from a total to a floor.

Declare the ceiling you are actually working against

--quota-bytes overrides the documented default. Use it when your account has a negotiated limit — OpenAI's docs invite you to ask for one — and use it when the platform republishes the number. The default is printed on every run with the word documented next to it so nobody mistakes it for something the API said.

Read the concentration before the total

The per-purpose table is the actionable half. It names the class holding the bytes, and those classes behave differently: batch inputs already expire after 30 days by default, while batch_output, fine-tune-results and assistants persist until somebody deletes them. Anthropic's file object has no purpose concept at all, so that table is one row there and the script says so rather than fabricating a breakdown.

Take the printed repair and run it yourself

Two lines come out: the deletion command for each candidate, and the preventive fix, which is an expiry set at upload time so the population stops being unbounded. Neither is executed. A deleted file is not recoverable, nothing in the metadata can tell an audit which files matter, and on OpenAI the deletion also detaches the file from every vector store holding it.

How to check it worked

Set an expiry on the next thing your pipeline uploads and re-run in a week. The total will not have moved much, and that is fine — the number that should move is the share of files carrying no expiry, because that one describes the trajectory rather than the position. Then delete one purpose class you have confirmed is dead and re-run again: the total steps down, the concentration table reorders, and the percentage against the ceiling becomes a number somebody is willing to look at monthly.

python3 file_store_quota_audit.py --provider openai --stale-days 90
# openai    4 page(s) read, 8,412 file(s), 1.5 TiB
#   measured: the sum of bytes over every page of GET /v1/files
#   documented: a ceiling of 2.5 TB per project, which no endpoint reports
# quota-warning        66.9% of the documented ceiling is in use, with about
#                      771.0 GiB of headroom before uploads start to fail
#   repair: sweep the purpose class named below, then set an expiry at upload
#           so the next two thirds take longer to arrive than the last did.
# purpose-dominates    batch_output is 64.1% of the store, 5,904 file(s)
#   repair: delete the ones whose job is finished and read, one at a time,
#           with DELETE /v1/files/{file_id}. Nothing here does that for you,
#           a deleted file cannot be recovered, and on OpenAI the deletion
#           also removes the file from every vector store holding it.
# file-near-cap        2 file(s) above 80% of the per-file cap
#                      file-9f1  464.6 MiB  fine-tune
#                      file-c07  441.2 MiB  fine-tune
#   repair: a second ceiling, unrelated to the total. Split these at source
#           rather than making room for them.
# no-expiry-policy     8,398 of 8,412 file(s) have no expires_at, and 6,110 of
#                      those are older than 90 day(s)
#   repair: upload with an expiry so this population stops being unbounded:
#           expires_after with an anchor of created_at on OpenAI (3600 to
#           2592000 seconds), expires_in_seconds on Anthropic.
# not-audited          ANTHROPIC_API_KEY not set, so that store was not
#                      audited. An unaudited store is not an empty one
# 1 store(s) audited, 4 finding(s)

The full code

One paged GET per provider and nine pure functions. epoch, which turns OpenAI's integer timestamps and Anthropic's RFC 3339 strings into one clock so the age readings work on both; file_row, which normalises bytes and size_bytes into one shape and refuses to invent a purpose for a provider that has none; totals and by_purpose, the two folds; human, binary units in one place rather than at every call site; grade_total, which takes the ceiling as an argument because it is documented rather than measured; grade_concentration, which finds the class worth sweeping; grade_outliers, which grades the second and separate per-file ceiling; and grade_expiry, the only one of the four that describes the future.

Get this script on GitHub Follow @allanninal Python and Node.js, with tests. Read only, it never writes. One of 97 LLM API fixes, free and open source.
file_store_quota_audit.py
"""Sum every page of the file store and grade it against a documented ceiling.

Read only. GET /v1/files and nothing else, on either provider. Nothing is
uploaded, nothing is deleted, and no file content is ever fetched: every
finding here is made of sizes, purposes, dates and ids.

Neither provider exposes the quota. There is no endpoint that returns a limit,
a consumed figure or a remaining one, so the ceiling below is a documented
constant that no request can confirm while the total is measured by summing a
field over every page. Those are different kinds of fact and the output keeps
them in different columns.

The ceilings also sit on different containers. OpenAI documents 2.5 TB per
project and no organization-wide limit at all; Anthropic documents 1 TB per
organization while its Files API is workspace scoped. So a single run measures
one project or one workspace, and says which.
"""
import argparse
import calendar
import logging
import os
import re
import sys
import time

import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("file_store_quota_audit")

ENDPOINTS = {"openai": "https://api.openai.com/v1/files",
             "anthropic": "https://api.anthropic.com/v1/files"}

# Documented ceilings, not readable ones. Overridable with --quota-bytes,
# because a negotiated limit and a republished docs page look identical here.
DOC_QUOTA_BYTES = {"openai": 2_500_000_000_000, "anthropic": 1_000_000_000_000}
DOC_QUOTA_LABEL = {"openai": "2.5 TB per project",
                   "anthropic": "1 TB per organization"}
DOC_FILE_CAP_BYTES = {"openai": 512_000_000, "anthropic": 500_000_000}
SIZE_FIELD = {"openai": "bytes", "anthropic": "size_bytes"}
KEY_ENV = {"openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY"}

FINDINGS = ("quota-critical", "quota-warning", "purpose-dominates",
            "file-near-cap", "no-expiry-policy")

_RFC3339 = re.compile(r"^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})"
                      r"(?:\.\d+)?(Z|z|[+-]\d{2}:?\d{2})?$")


def epoch(value):
    """Seconds since the epoch from either provider's shape. Pure.

    OpenAI returns integer Unix seconds. Anthropic returns RFC 3339 strings.
    Returns 0 for anything unparseable, and 0 means unknown everywhere it is
    read rather than meaning 1970.
    """
    if value is None or value == "" or isinstance(value, bool):
        return 0
    if isinstance(value, (int, float)):
        try:
            return max(0, int(value))
        except (TypeError, ValueError, OverflowError):
            return 0
    m = _RFC3339.match(str(value).strip())
    if not m:
        return 0
    try:
        base = calendar.timegm(tuple(int(g) for g in m.groups()[:6]) + (0, 0, 0))
    except (TypeError, ValueError):
        return 0
    off = m.group(7)
    if off and off not in ("Z", "z"):
        digits = off[1:].replace(":", "")
        shift = int(digits[:2]) * 3600 + int(digits[2:4]) * 60
        base -= shift if off[0] == "+" else -shift
    return max(0, base)


def file_row(body, provider):
    """One file object, normalised. Pure. Two providers, one shape.

    OpenAI calls the size `bytes` and carries a `purpose`; Anthropic calls it
    `size_bytes` and has no purpose concept at all, so this refuses to invent
    one. `expires_at` is optional rather than nullable on OpenAI, meaning the
    key is simply absent on a file with no expiry, and on Anthropic it is
    absent whenever the files-api-2025-04-14 beta header was sent.
    """
    body = body if isinstance(body, dict) else {}
    try:
        size = int(body.get(SIZE_FIELD.get(provider, "bytes")))
    except (TypeError, ValueError):
        size = 0
    return {"id": str(body.get("id") or ""),
            "filename": str(body.get("filename") or ""),
            "size": max(0, size),
            "purpose": str(body.get("purpose") or "unclassified"),
            "created_at": epoch(body.get("created_at")),
            "expires_at": epoch(body.get("expires_at")) or None,
            "expiry_reported": "expires_at" in body}


def human(size):
    """Binary units, one decimal. Pure. Used everywhere rather than inlined."""
    try:
        n = float(size)
    except (TypeError, ValueError):
        return "0 B"
    for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
        if abs(n) < 1024 or unit == "TiB":
            return "%d B" % int(n) if unit == "B" else "%.1f %s" % (n, unit)
        n /= 1024.0
    return "%.1f TiB" % n


def totals(rows):
    """Count and summed bytes. Pure."""
    rows = rows or []
    return {"count": len(rows), "bytes": sum(int(r.get("size") or 0) for r in rows)}


def by_purpose(rows):
    """Per-purpose count and bytes, largest first. Pure."""
    acc = {}
    for row in rows or []:
        key = str(row.get("purpose") or "unclassified")
        cur = acc.setdefault(key, {"count": 0, "bytes": 0})
        cur["count"] += 1
        cur["bytes"] += int(row.get("size") or 0)
    return sorted(([k, v["count"], v["bytes"]] for k, v in acc.items()),
                  key=lambda item: (-item[2], item[0]))


def grade_total(total_bytes, quota_bytes, warn_share=0.60, critical_share=0.85):
    """Share of a documented ceiling. Pure. The ceiling is an argument.

    An argument rather than a constant because the number came out of a docs
    page: it can be renegotiated for one account and republished for everyone,
    and neither event is visible from any GET.
    """
    try:
        quota, used = int(quota_bytes), int(total_bytes)
    except (TypeError, ValueError):
        quota, used = 0, 0
    if quota <= 0:
        return ("quota-unknown", "no usable ceiling was supplied, so the total "
                                 "is a number without a denominator")
    share = used / float(quota)
    detail = ("%.1f%% of the documented ceiling is in use, with about %s of "
              "headroom before uploads start to fail"
              % (share * 100, human(max(0, quota - used))))
    if share >= critical_share:
        return ("quota-critical", detail)
    if share >= warn_share:
        return ("quota-warning", detail)
    return ("quota-headroom",
            "%.1f%% of the documented ceiling is in use, %s of headroom"
            % (share * 100, human(max(0, quota - used))))


def grade_concentration(purposes, total_bytes, share=0.40):
    """The purpose class worth sweeping first. Pure."""
    try:
        total = int(total_bytes)
    except (TypeError, ValueError):
        total = 0
    if not purposes or total <= 0:
        return ("purpose-even", "nothing to concentrate: the store is empty or "
                                "carries no size information")
    name, count, size = purposes[0]
    got = size / float(total)
    if got < share:
        return ("purpose-even",
                "no single purpose holds more than %.0f%% of the store; the "
                "largest is %s at %.1f%%" % (share * 100, name, got * 100))
    return ("purpose-dominates",
            "%s is %.1f%% of the store, %d file(s)" % (name, got * 100, count))


def grade_outliers(rows, cap_bytes, warn_share=0.80):
    """The second ceiling, per file and not a fraction of the first. Pure."""
    try:
        cap = int(cap_bytes)
    except (TypeError, ValueError):
        cap = 0
    if cap <= 0:
        return ("cap-unknown", "no per-file cap was supplied", [])
    floor = cap * warn_share
    big = sorted((r for r in rows or [] if int(r.get("size") or 0) >= floor),
                 key=lambda r: -int(r.get("size") or 0))
    if not big:
        return ("file-sizes-fine",
                "no file is within %.0f%% of the per-file cap"
                % (warn_share * 100), [])
    return ("file-near-cap",
            "%d file(s) above %.0f%% of the per-file cap"
            % (len(big), warn_share * 100), big)


def grade_expiry(rows, now, stale_days):
    """The only reading here that describes the future. Pure."""
    rows = rows or []
    if not rows:
        return ("expiry-none", "the store is empty")
    unexpiring = [r for r in rows if not r.get("expires_at")]
    if not unexpiring:
        return ("expiry-covered",
                "every file carries an expires_at, so this store has a "
                "lifecycle rather than a trajectory")
    cutoff = int(now) - int(stale_days) * 86400
    stale = [r for r in unexpiring
             if r.get("created_at") and int(r["created_at"]) < cutoff]
    return ("no-expiry-policy",
            "%d of %d file(s) have no expires_at, and %d of those are older "
            "than %d day(s)"
            % (len(unexpiring), len(rows), len(stale), int(stale_days)))


def repair_lines(state):
    """The repair for one verdict. Pure. Printed, never performed."""
    if state in ("quota-critical", "quota-warning"):
        return ["sweep the purpose class named below, then set an expiry at "
                "upload so the next two thirds take longer to arrive than the "
                "last did."]
    if state == "purpose-dominates":
        return ["delete the ones whose job is finished and read, one at a "
                "time, with DELETE /v1/files/{file_id}. Nothing here does that "
                "for you, a deleted file cannot be recovered, and on OpenAI "
                "the deletion also removes the file from every vector store "
                "holding it."]
    if state == "file-near-cap":
        return ["a second ceiling, unrelated to the total. Split these at "
                "source rather than making room for them."]
    if state == "no-expiry-policy":
        return ["upload with an expiry so this population stops being "
                "unbounded: expires_after with an anchor of created_at on "
                "OpenAI (3600 to 2592000 seconds), expires_in_seconds on "
                "Anthropic (3600 to 7776000).",
                "for what is already there, confirm by hand and then delete. "
                "Nothing in the metadata can tell an audit which files matter."]
    if state == "quota-unknown":
        return ["pass --quota-bytes. Without a denominator this run is an "
                "inventory rather than an audit."]
    return []


def fetch_openai(key, max_pages, timeout=30):
    """Page GET /v1/files on `after`. Returns (rows, pages, complete)."""
    rows, cursor, pages = [], None, 0
    while pages < max_pages:
        params = {"limit": 10000, "order": "asc"}
        if cursor:
            params["after"] = cursor
        try:
            r = requests.get(ENDPOINTS["openai"], params=params,
                             headers={"Authorization": "Bearer " + key},
                             timeout=timeout)
        except requests.RequestException as exc:
            log.error("openai listing failed: %s", exc)
            return (rows, pages, False)
        if r.status_code != 200:
            log.error("openai listing returned HTTP %s", r.status_code)
            return (rows, pages, False)
        body = r.json() if r.content else {}
        data = body.get("data") or []
        pages += 1
        rows.extend(file_row(item, "openai") for item in data)
        # has_more is a required field on this response, so it is authoritative
        # where it appears. The short-page fallback is only for a response that
        # does not honour its own schema, which is worth surviving rather than
        # trusting blindly.
        if body.get("has_more") is False or not data:
            return (rows, pages, True)
        if "has_more" not in body and len(data) < params["limit"]:
            return (rows, pages, True)
        cursor = data[-1].get("id")
        if not cursor:
            return (rows, pages, True)
    return (rows, pages, False)


def fetch_anthropic(key, max_pages, timeout=30):
    """Page GET /v1/files on `page`/`next_page`. Returns (rows, pages, complete).

    Sent without the files-api-2025-04-14 beta header on purpose: with it the
    response reverts to the older cursor shape and expires_at is not returned
    at all, which would silently remove a field this audit reads.
    """
    rows, page, pages = [], None, 0
    headers = {"x-api-key": key, "anthropic-version": "2023-06-01"}
    while pages < max_pages:
        params = {"limit": 1000}
        if page:
            params["page"] = page
        try:
            r = requests.get(ENDPOINTS["anthropic"], params=params,
                             headers=headers, timeout=timeout)
        except requests.RequestException as exc:
            log.error("anthropic listing failed: %s", exc)
            return (rows, pages, False)
        if r.status_code != 200:
            log.error("anthropic listing returned HTTP %s", r.status_code)
            return (rows, pages, False)
        body = r.json() if r.content else {}
        data = body.get("data") or []
        pages += 1
        rows.extend(file_row(item, "anthropic") for item in data)
        page = body.get("next_page")
        if not page:
            return (rows, pages, True)
    return (rows, pages, False)


def report(provider, rows, pages, complete, args, now):
    """Print one store's verdicts. Returns the number of findings."""
    quota = args.quota_bytes or DOC_QUOTA_BYTES[provider]
    cap = args.file_cap_bytes or DOC_FILE_CAP_BYTES[provider]
    tot = totals(rows)
    log.info("%-9s %d page(s) read, %d file(s), %s",
             provider, pages, tot["count"], human(tot["bytes"]))
    log.info("  measured: the sum of %s over every page of GET /v1/files",
             SIZE_FIELD[provider])
    log.info("  documented: a ceiling of %s, which no endpoint reports",
             DOC_QUOTA_LABEL[provider])
    if not complete:
        log.warning("  incomplete: paging stopped early, so %s is a floor and "
                    "not a total", human(tot["bytes"]))

    outlier_state, outlier_detail, big = grade_outliers(rows, cap)
    grades = [grade_total(tot["bytes"], quota),
              grade_concentration(by_purpose(rows), tot["bytes"]),
              (outlier_state, outlier_detail),
              grade_expiry(rows, now, args.stale_days)]

    findings = 0
    for state, detail in grades:
        emit = log.warning if state in FINDINGS else log.info
        emit("%-20s %s", state, detail)
        if state == "file-near-cap":
            for row in big[:5]:
                emit("%-20s %s  %s  %s", "", row["id"], human(row["size"]),
                     row["purpose"])
        for line in repair_lines(state):
            emit("  repair: %s", line)
        if state in FINDINGS:
            findings += 1
    return findings


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--provider", choices=("openai", "anthropic", "both"),
                    default="both", help="which file store to audit")
    ap.add_argument("--quota-bytes", type=int, default=0,
                    help="override the documented ceiling for your account")
    ap.add_argument("--file-cap-bytes", type=int, default=0,
                    help="override the documented per-file cap")
    ap.add_argument("--stale-days", type=int, default=90,
                    help="age at which a file with no expiry is worth listing")
    ap.add_argument("--max-pages", type=int, default=50,
                    help="stop after this many pages and report a floor")
    args = ap.parse_args()

    now = int(time.time())
    wanted = ("openai", "anthropic") if args.provider == "both" else (args.provider,)
    ran = findings = 0
    for provider in wanted:
        key = os.environ.get(KEY_ENV[provider])
        if not key:
            log.info("%-20s %s not set, so that store was not audited. An "
                     "unaudited store is not an empty one",
                     "not-audited", KEY_ENV[provider])
            continue
        fetch = fetch_openai if provider == "openai" else fetch_anthropic
        rows, pages, complete = fetch(key, args.max_pages)
        findings += report(provider, rows, pages, complete, args, now)
        ran += 1

    if not ran:
        log.error("set OPENAI_API_KEY (a project read key) or ANTHROPIC_API_KEY "
                  "(a key with access to the workspace). Every call is a GET of "
                  "/v1/files")
        return 2
    log.info("%d store(s) audited, %d finding(s)", ran, findings)
    return 1 if findings else 0


if __name__ == "__main__":
    sys.exit(main())
file-store-quota-audit.mjs
/**
 * Sum every page of the file store and grade it against a documented ceiling.
 *
 * Read only. GET /v1/files and nothing else, on either provider. Nothing is
 * uploaded, nothing is deleted, and no file content is ever fetched.
 *
 * Neither provider exposes the quota, so the ceiling is a documented constant
 * that no request can confirm while the total is measured by summing a field
 * over every page. The output keeps those two in different columns.
 */
const ENDPOINTS = {
  openai: 'https://api.openai.com/v1/files',
  anthropic: 'https://api.anthropic.com/v1/files',
};

export const DOC_QUOTA_BYTES = { openai: 2_500_000_000_000, anthropic: 1_000_000_000_000 };
export const DOC_QUOTA_LABEL = { openai: '2.5 TB per project',
                                 anthropic: '1 TB per organization' };
export const DOC_FILE_CAP_BYTES = { openai: 512_000_000, anthropic: 500_000_000 };
const SIZE_FIELD = { openai: 'bytes', anthropic: 'size_bytes' };
const KEY_ENV = { openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY' };

const FINDINGS = new Set(['quota-critical', 'quota-warning', 'purpose-dominates',
  'file-near-cap', 'no-expiry-policy']);

/** Seconds since the epoch from either provider's shape. Pure. */
export function epoch(value) {
  if (value === null || value === undefined || value === '' || typeof value === 'boolean') {
    return 0;
  }
  if (typeof value === 'number') {
    return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
  }
  const ms = Date.parse(String(value).trim());
  return Number.isFinite(ms) ? Math.max(0, Math.trunc(ms / 1000)) : 0;
}

/** One file object, normalised. Pure. Two providers, one shape. */
export function fileRow(body, provider) {
  const row = (body && typeof body === 'object') ? body : {};
  const size = Number(row[SIZE_FIELD[provider] ?? 'bytes']);
  const expires = epoch(row.expires_at);
  return {
    id: String(row.id ?? ''),
    filename: String(row.filename ?? ''),
    size: Number.isFinite(size) ? Math.max(0, Math.trunc(size)) : 0,
    purpose: String(row.purpose ?? 'unclassified'),
    created_at: epoch(row.created_at),
    expires_at: expires || null,
    expiry_reported: Object.prototype.hasOwnProperty.call(row, 'expires_at'),
  };
}

/** Binary units, one decimal. Pure. */
export function human(size) {
  let n = Number(size);
  if (!Number.isFinite(n)) return '0 B';
  for (const unit of ['B', 'KiB', 'MiB', 'GiB', 'TiB']) {
    if (Math.abs(n) < 1024 || unit === 'TiB') {
      return unit === 'B' ? `${Math.trunc(n)} B` : `${n.toFixed(1)} ${unit}`;
    }
    n /= 1024;
  }
  return `${n.toFixed(1)} TiB`;
}

/** Count and summed bytes. Pure. */
export function totals(rows) {
  const list = rows ?? [];
  return { count: list.length,
           bytes: list.reduce((a, r) => a + Number(r?.size ?? 0), 0) };
}

/** Per-purpose count and bytes, largest first. Pure. */
export function byPurpose(rows) {
  const acc = new Map();
  for (const row of rows ?? []) {
    const key = String(row?.purpose ?? 'unclassified');
    const cur = acc.get(key) ?? { count: 0, bytes: 0 };
    cur.count += 1;
    cur.bytes += Number(row?.size ?? 0);
    acc.set(key, cur);
  }
  return [...acc.entries()]
    .map(([k, v]) => [k, v.count, v.bytes])
    .sort((a, b) => (b[2] - a[2]) || String(a[0]).localeCompare(String(b[0])));
}

/** Share of a documented ceiling. Pure. The ceiling is an argument. */
export function gradeTotal(totalBytes, quotaBytes, warnShare = 0.60, criticalShare = 0.85) {
  const quota = Number(quotaBytes);
  const used = Number(totalBytes);
  if (!Number.isFinite(quota) || !Number.isFinite(used) || quota <= 0) {
    return ['quota-unknown',
      'no usable ceiling was supplied, so the total is a number without a denominator'];
  }
  const share = used / quota;
  const headroom = human(Math.max(0, quota - used));
  const detail = `${(share * 100).toFixed(1)}% of the documented ceiling is in use, `
    + `with about ${headroom} of headroom before uploads start to fail`;
  if (share >= criticalShare) return ['quota-critical', detail];
  if (share >= warnShare) return ['quota-warning', detail];
  return ['quota-headroom',
    `${(share * 100).toFixed(1)}% of the documented ceiling is in use, ${headroom} of headroom`];
}

/** The purpose class worth sweeping first. Pure. */
export function gradeConcentration(purposes, totalBytes, share = 0.40) {
  const total = Number(totalBytes) || 0;
  if (!(purposes ?? []).length || total <= 0) {
    return ['purpose-even',
      'nothing to concentrate: the store is empty or carries no size information'];
  }
  const [name, count, size] = purposes[0];
  const got = size / total;
  if (got < share) {
    return ['purpose-even',
      `no single purpose holds more than ${(share * 100).toFixed(0)}% of the store; `
      + `the largest is ${name} at ${(got * 100).toFixed(1)}%`];
  }
  return ['purpose-dominates',
    `${name} is ${(got * 100).toFixed(1)}% of the store, ${count} file(s)`];
}

/** The second ceiling, per file and not a fraction of the first. Pure. */
export function gradeOutliers(rows, capBytes, warnShare = 0.80) {
  const cap = Number(capBytes);
  if (!Number.isFinite(cap) || cap <= 0) {
    return ['cap-unknown', 'no per-file cap was supplied', []];
  }
  const floor = cap * warnShare;
  const big = (rows ?? []).filter((r) => Number(r?.size ?? 0) >= floor)
    .sort((a, b) => Number(b.size) - Number(a.size));
  if (!big.length) {
    return ['file-sizes-fine',
      `no file is within ${(warnShare * 100).toFixed(0)}% of the per-file cap`, []];
  }
  return ['file-near-cap',
    `${big.length} file(s) above ${(warnShare * 100).toFixed(0)}% of the per-file cap`,
    big];
}

/** The only reading here that describes the future. Pure. */
export function gradeExpiry(rows, now, staleDays) {
  const list = rows ?? [];
  if (!list.length) return ['expiry-none', 'the store is empty'];
  const unexpiring = list.filter((r) => !r?.expires_at);
  if (!unexpiring.length) {
    return ['expiry-covered',
      'every file carries an expires_at, so this store has a lifecycle rather '
      + 'than a trajectory'];
  }
  const cutoff = Number(now) - Number(staleDays) * 86400;
  const stale = unexpiring.filter((r) => Number(r?.created_at ?? 0) > 0
    && Number(r.created_at) < cutoff);
  return ['no-expiry-policy',
    `${unexpiring.length} of ${list.length} file(s) have no expires_at, and `
    + `${stale.length} of those are older than ${Number(staleDays)} day(s)`];
}

/** The repair for one verdict. Pure. Printed, never performed. */
export function repairLines(state) {
  if (state === 'quota-critical' || state === 'quota-warning') {
    return ['sweep the purpose class named below, then set an expiry at upload so '
      + 'the next two thirds take longer to arrive than the last did.'];
  }
  if (state === 'purpose-dominates') {
    return ['delete the ones whose job is finished and read, one at a time, with '
      + 'DELETE /v1/files/{file_id}. Nothing here does that for you, a deleted '
      + 'file cannot be recovered, and on OpenAI the deletion also removes the '
      + 'file from every vector store holding it.'];
  }
  if (state === 'file-near-cap') {
    return ['a second ceiling, unrelated to the total. Split these at source '
      + 'rather than making room for them.'];
  }
  if (state === 'no-expiry-policy') {
    return ['upload with an expiry so this population stops being unbounded: '
      + 'expires_after with an anchor of created_at on OpenAI (3600 to 2592000 '
      + 'seconds), expires_in_seconds on Anthropic (3600 to 7776000).',
    'for what is already there, confirm by hand and then delete. Nothing in the '
      + 'metadata can tell an audit which files matter.'];
  }
  if (state === 'quota-unknown') {
    return ['pass --quota-bytes. Without a denominator this run is an inventory '
      + 'rather than an audit.'];
  }
  return [];
}

async function fetchOpenai(key, maxPages) {
  const rows = [];
  let cursor = null;
  let pages = 0;
  while (pages < maxPages) {
    const url = new URL(ENDPOINTS.openai);
    url.searchParams.set('limit', '10000');
    url.searchParams.set('order', 'asc');
    if (cursor) url.searchParams.set('after', cursor);
    let res;
    try {
      res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
    } catch (err) {
      console.error(`openai listing failed: ${err.message}`);
      return [rows, pages, false];
    }
    if (res.status !== 200) {
      console.error(`openai listing returned HTTP ${res.status}`);
      return [rows, pages, false];
    }
    const body = await res.json().catch(() => ({}));
    const data = body.data ?? [];
    pages += 1;
    for (const item of data) rows.push(fileRow(item, 'openai'));
    if (body.has_more === false || !data.length) return [rows, pages, true];
    if (!('has_more' in body) && data.length < 10000) return [rows, pages, true];
    cursor = data[data.length - 1]?.id;
    if (!cursor) return [rows, pages, true];
  }
  return [rows, pages, false];
}

async function fetchAnthropic(key, maxPages) {
  const rows = [];
  let page = null;
  let pages = 0;
  const headers = { 'x-api-key': key, 'anthropic-version': '2023-06-01' };
  while (pages < maxPages) {
    const url = new URL(ENDPOINTS.anthropic);
    url.searchParams.set('limit', '1000');
    if (page) url.searchParams.set('page', page);
    let res;
    try {
      res = await fetch(url, { headers });
    } catch (err) {
      console.error(`anthropic listing failed: ${err.message}`);
      return [rows, pages, false];
    }
    if (res.status !== 200) {
      console.error(`anthropic listing returned HTTP ${res.status}`);
      return [rows, pages, false];
    }
    const body = await res.json().catch(() => ({}));
    const data = body.data ?? [];
    pages += 1;
    for (const item of data) rows.push(fileRow(item, 'anthropic'));
    page = body.next_page;
    if (!page) return [rows, pages, true];
  }
  return [rows, pages, false];
}

function report(provider, rows, pages, complete, opts, now) {
  const quota = opts.quotaBytes || DOC_QUOTA_BYTES[provider];
  const cap = opts.fileCapBytes || DOC_FILE_CAP_BYTES[provider];
  const tot = totals(rows);
  console.log(`${provider.padEnd(9)} ${pages} page(s) read, ${tot.count} file(s), `
    + `${human(tot.bytes)}`);
  console.log(`  measured: the sum of ${SIZE_FIELD[provider]} over every page of `
    + 'GET /v1/files');
  console.log(`  documented: a ceiling of ${DOC_QUOTA_LABEL[provider]}, which no `
    + 'endpoint reports');
  if (!complete) {
    console.log(`  incomplete: paging stopped early, so ${human(tot.bytes)} is a `
      + 'floor and not a total');
  }

  const [outlierState, outlierDetail, big] = gradeOutliers(rows, cap);
  const grades = [gradeTotal(tot.bytes, quota),
                  gradeConcentration(byPurpose(rows), tot.bytes),
                  [outlierState, outlierDetail],
                  gradeExpiry(rows, now, opts.staleDays)];
  let findings = 0;
  for (const [state, detail] of grades) {
    console.log(`${state.padEnd(20)} ${detail}`);
    if (state === 'file-near-cap') {
      for (const row of big.slice(0, 5)) {
        console.log(`${''.padEnd(20)} ${row.id}  ${human(row.size)}  ${row.purpose}`);
      }
    }
    for (const line of repairLines(state)) console.log(`  repair: ${line}`);
    if (FINDINGS.has(state)) findings += 1;
  }
  return findings;
}

function args(argv) {
  const out = { provider: 'both', quotaBytes: 0, fileCapBytes: 0, staleDays: 90,
                maxPages: 50 };
  for (let i = 0; i < argv.length; i += 1) {
    if (argv[i] === '--provider') out.provider = argv[i += 1];
    else if (argv[i] === '--quota-bytes') out.quotaBytes = Number(argv[i += 1]);
    else if (argv[i] === '--file-cap-bytes') out.fileCapBytes = Number(argv[i += 1]);
    else if (argv[i] === '--stale-days') out.staleDays = Number(argv[i += 1]);
    else if (argv[i] === '--max-pages') out.maxPages = Number(argv[i += 1]);
  }
  return out;
}

async function main() {
  const opts = args(process.argv.slice(2));
  const now = Math.trunc(Date.now() / 1000);
  const wanted = opts.provider === 'both' ? ['openai', 'anthropic'] : [opts.provider];
  let ran = 0;
  let findings = 0;
  for (const provider of wanted) {
    const key = process.env[KEY_ENV[provider]];
    if (!key) {
      console.log(`${'not-audited'.padEnd(20)} ${KEY_ENV[provider]} not set, so that `
        + 'store was not audited. An unaudited store is not an empty one');
      continue;
    }
    const [rows, pages, complete] = provider === 'openai'
      ? await fetchOpenai(key, opts.maxPages)
      : await fetchAnthropic(key, opts.maxPages);
    findings += report(provider, rows, pages, complete, opts, now);
    ran += 1;
  }
  if (!ran) {
    console.error('set OPENAI_API_KEY (a project read key) or ANTHROPIC_API_KEY '
      + '(a key with access to the workspace). Every call is a GET of /v1/files');
    process.exitCode = 2;
    return;
  }
  console.log(`${ran} store(s) audited, ${findings} finding(s)`);
  process.exitCode = findings ? 1 : 0;
}

if (import.meta.url === `file://${process.argv[1]}`) await main();

Add a test

The first test is the one that keeps the note honest: the ceiling is an argument, so the same measured total grades differently against a different documented limit, and a run with no usable ceiling reports an inventory rather than a percentage. The second is the normaliser, which turns OpenAI's bytes and Anthropic's size_bytes into one shape, parses both an integer timestamp and an RFC 3339 string through one clock, and must not invent a purpose for a provider that has no such concept. Then concentration, which only fires when one class really dominates, because a report that always fires is a report nobody reads. Then the per-file cap, asserted independent of the total: a store at 3% of quota with one enormous file is still a finding. Then expiry, the only grader here about the future, including the store where everything expires and there is nothing to say. And last the repair lines, checked to name a deletion and never to perform one.

test_file_store_quota_audit.py
from file_store_quota_audit import (by_purpose, epoch, file_row,
                                    grade_concentration, grade_expiry,
                                    grade_outliers, grade_total, human,
                                    repair_lines, totals)

NOW = 1_800_000_000
DAY = 86400
GIB = 1024 ** 3


def oai(fid, size, purpose="batch", days_old=1, expires=None):
    return file_row({"id": fid, "bytes": size, "purpose": purpose,
                     "filename": fid + ".jsonl",
                     "created_at": NOW - int(days_old * DAY),
                     "expires_at": expires}, "openai")


def test_the_ceiling_is_an_argument_because_no_endpoint_reports_it():
    used = 90 * GIB
    tight, detail = grade_total(used, 100 * GIB)
    assert tight == "quota-critical"
    assert "90.0%" in detail and "headroom" in detail
    # Same measured total, different documented ceiling, different verdict.
    assert grade_total(used, 1000 * GIB)[0] == "quota-headroom"
    assert grade_total(used, 140 * GIB)[0] == "quota-warning"
    # No denominator at all is an inventory, not a percentage.
    state, detail = grade_total(used, 0)
    assert state == "quota-unknown"
    assert "without a denominator" in detail
    assert any("--quota-bytes" in line for line in repair_lines(state))


def test_two_providers_normalise_to_one_shape_and_one_clock():
    a = file_row({"id": "file-a1", "bytes": 2048, "purpose": "batch_output",
                  "created_at": 1_700_000_000, "expires_at": None}, "openai")
    b = file_row({"id": "file_b2", "size_bytes": 2048, "filename": "doc.pdf",
                  "created_at": "2023-11-14T22:13:20Z",
                  "expires_at": None}, "anthropic")
    assert a["size"] == b["size"] == 2048
    assert a["purpose"] == "batch_output"
    # Anthropic has no purpose concept, so the row says so rather than guessing.
    assert b["purpose"] == "unclassified"
    # One clock: an integer and an RFC 3339 string land on the same second.
    assert a["created_at"] == b["created_at"] == 1_700_000_000
    assert epoch("2023-11-14T22:13:20+00:00") == 1_700_000_000
    assert epoch("2023-11-14T23:13:20+01:00") == 1_700_000_000
    assert epoch(None) == 0 and epoch("") == 0 and epoch("last tuesday") == 0
    assert a["expires_at"] is None and a["expiry_reported"] is True
    # A row with no expires_at key at all: OpenAI omits it rather than nulling.
    assert file_row({"id": "file-c3", "bytes": 1}, "openai")["expiry_reported"] is False
    assert file_row(None, "openai")["size"] == 0
    assert file_row({"bytes": "nonsense"}, "openai")["size"] == 0
    assert human(2048) == "2.0 KiB" and human(0) == "0 B" and human(None) == "0 B"


def test_concentration_only_fires_when_one_class_really_dominates():
    lopsided = [oai("file-1", 90 * GIB, "batch_output"),
                oai("file-2", 5 * GIB, "fine-tune"),
                oai("file-3", 5 * GIB, "user_data")]
    tot = totals(lopsided)
    assert tot == {"count": 3, "bytes": 100 * GIB}
    ranked = by_purpose(lopsided)
    assert ranked[0][0] == "batch_output" and ranked[0][1] == 1
    state, detail = grade_concentration(ranked, tot["bytes"])
    assert state == "purpose-dominates"
    assert "batch_output is 90.0%" in detail
    assert any("DELETE /v1/files/{file_id}" in line for line in repair_lines(state))
    # An evenly spread store has nothing to sweep first.
    even = [oai("file-4", 10 * GIB, "batch"), oai("file-5", 10 * GIB, "fine-tune"),
            oai("file-6", 10 * GIB, "user_data")]
    flat, flat_detail = grade_concentration(by_purpose(even), totals(even)["bytes"])
    assert flat == "purpose-even"
    assert "largest is" in flat_detail
    assert grade_concentration([], 0)[0] == "purpose-even"


def test_the_per_file_cap_is_a_second_ceiling_and_not_a_share_of_the_first():
    rows = [oai("file-9f1", 487_000_000, "fine-tune"), oai("file-a2", 1024)]
    # A fraction of a percent of a huge quota, and still a finding.
    assert grade_total(totals(rows)["bytes"], 16 * 1024 * GIB)[0] == "quota-headroom"
    state, detail, big = grade_outliers(rows, 512_000_000)
    assert state == "file-near-cap"
    assert "1 file(s)" in detail and "80%" in detail
    assert [row["id"] for row in big] == ["file-9f1"]
    assert any("second ceiling" in line for line in repair_lines(state))
    assert grade_outliers(rows, 16 * GIB)[0] == "file-sizes-fine"
    assert grade_outliers(rows, 0)[0] == "cap-unknown"


def test_expiry_is_the_only_grader_that_describes_the_future():
    stale = [oai("file-1", GIB, days_old=200), oai("file-2", GIB, days_old=200),
             oai("file-3", GIB, days_old=2)]
    state, detail = grade_expiry(stale, NOW, 90)
    assert state == "no-expiry-policy"
    assert "3 of 3 file(s) have no expires_at" in detail
    assert "2 of those are older than 90 day(s)" in detail
    assert any("expires_in_seconds" in line for line in repair_lines(state))
    # A store where everything expires has a lifecycle and is not this note.
    covered = [oai("file-4", GIB, expires=NOW + 10 * DAY)]
    clean, clean_detail = grade_expiry(covered, NOW, 90)
    assert clean == "expiry-covered"
    assert "lifecycle" in clean_detail
    assert repair_lines(clean) == []
    assert grade_expiry([], NOW, 90)[0] == "expiry-none"


def test_every_repair_is_printed_and_none_of_them_reclaims_anything():
    for state in ("quota-critical", "quota-warning", "purpose-dominates",
                  "file-near-cap", "no-expiry-policy", "quota-unknown"):
        lines = repair_lines(state)
        assert lines, state
        assert all(isinstance(line, str) and line for line in lines)
    assert any("cannot be recovered" in line
               for line in repair_lines("purpose-dominates"))
    assert repair_lines("quota-headroom") == []
    assert repair_lines("expiry-covered") == []
file-store-quota-audit.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { byPurpose, epoch, fileRow, gradeConcentration, gradeExpiry, gradeOutliers,
         gradeTotal, human, repairLines, totals } from './file-store-quota-audit.mjs';

const NOW = 1_800_000_000;
const DAY = 86400;
const GIB = 1024 ** 3;

const oai = (id, size, purpose = 'batch', daysOld = 1, expires = null) => fileRow({
  id, bytes: size, purpose, filename: `${id}.jsonl`,
  created_at: NOW - Math.trunc(daysOld * DAY), expires_at: expires,
}, 'openai');

test('the ceiling is an argument because no endpoint reports it', () => {
  const used = 90 * GIB;
  const [tight, detail] = gradeTotal(used, 100 * GIB);
  assert.equal(tight, 'quota-critical');
  assert.ok(detail.includes('90.0%') && detail.includes('headroom'));
  assert.equal(gradeTotal(used, 1000 * GIB)[0], 'quota-headroom');
  assert.equal(gradeTotal(used, 140 * GIB)[0], 'quota-warning');
  const [state, why] = gradeTotal(used, 0);
  assert.equal(state, 'quota-unknown');
  assert.ok(why.includes('without a denominator'));
  assert.ok(repairLines(state).some((l) => l.includes('--quota-bytes')));
});

test('two providers normalise to one shape and one clock', () => {
  const a = fileRow({ id: 'file-a1', bytes: 2048, purpose: 'batch_output',
                      created_at: 1700000000, expires_at: null }, 'openai');
  const b = fileRow({ id: 'file_b2', size_bytes: 2048, filename: 'doc.pdf',
                      created_at: '2023-11-14T22:13:20Z', expires_at: null },
                    'anthropic');
  assert.equal(a.size, 2048);
  assert.equal(b.size, 2048);
  assert.equal(a.purpose, 'batch_output');
  assert.equal(b.purpose, 'unclassified');
  assert.equal(a.created_at, 1700000000);
  assert.equal(b.created_at, 1700000000);
  assert.equal(epoch('2023-11-14T22:13:20+00:00'), 1700000000);
  assert.equal(epoch('2023-11-14T23:13:20+01:00'), 1700000000);
  assert.equal(epoch(null), 0);
  assert.equal(epoch(''), 0);
  assert.equal(epoch('last tuesday'), 0);
  assert.equal(a.expires_at, null);
  assert.equal(a.expiry_reported, true);
  assert.equal(fileRow({ id: 'file-c3', bytes: 1 }, 'openai').expiry_reported, false);
  assert.equal(fileRow(null, 'openai').size, 0);
  assert.equal(fileRow({ bytes: 'nonsense' }, 'openai').size, 0);
  assert.equal(human(2048), '2.0 KiB');
  assert.equal(human(0), '0 B');
  assert.equal(human(null), '0 B');
});

test('concentration only fires when one class really dominates', () => {
  const lopsided = [oai('file-1', 90 * GIB, 'batch_output'),
                    oai('file-2', 5 * GIB, 'fine-tune'),
                    oai('file-3', 5 * GIB, 'user_data')];
  const tot = totals(lopsided);
  assert.deepEqual(tot, { count: 3, bytes: 100 * GIB });
  const ranked = byPurpose(lopsided);
  assert.equal(ranked[0][0], 'batch_output');
  assert.equal(ranked[0][1], 1);
  const [state, detail] = gradeConcentration(ranked, tot.bytes);
  assert.equal(state, 'purpose-dominates');
  assert.ok(detail.includes('batch_output is 90.0%'));
  assert.ok(repairLines(state).some((l) => l.includes('DELETE /v1/files/{file_id}')));
  const even = [oai('file-4', 10 * GIB, 'batch'), oai('file-5', 10 * GIB, 'fine-tune'),
                oai('file-6', 10 * GIB, 'user_data')];
  const [flat, flatDetail] = gradeConcentration(byPurpose(even), totals(even).bytes);
  assert.equal(flat, 'purpose-even');
  assert.ok(flatDetail.includes('largest is'));
  assert.equal(gradeConcentration([], 0)[0], 'purpose-even');
});

test('the per file cap is a second ceiling and not a share of the first', () => {
  const rows = [oai('file-9f1', 487000000, 'fine-tune'), oai('file-a2', 1024)];
  assert.equal(gradeTotal(totals(rows).bytes, 16 * 1024 * GIB)[0], 'quota-headroom');
  const [state, detail, big] = gradeOutliers(rows, 512000000);
  assert.equal(state, 'file-near-cap');
  assert.ok(detail.includes('1 file(s)') && detail.includes('80%'));
  assert.deepEqual(big.map((r) => r.id), ['file-9f1']);
  assert.ok(repairLines(state).some((l) => l.includes('second ceiling')));
  assert.equal(gradeOutliers(rows, 16 * GIB)[0], 'file-sizes-fine');
  assert.equal(gradeOutliers(rows, 0)[0], 'cap-unknown');
});

test('expiry is the only grader that describes the future', () => {
  const stale = [oai('file-1', GIB, 'batch', 200), oai('file-2', GIB, 'batch', 200),
                 oai('file-3', GIB, 'batch', 2)];
  const [state, detail] = gradeExpiry(stale, NOW, 90);
  assert.equal(state, 'no-expiry-policy');
  assert.ok(detail.includes('3 of 3 file(s) have no expires_at'));
  assert.ok(detail.includes('2 of those are older than 90 day(s)'));
  assert.ok(repairLines(state).some((l) => l.includes('expires_in_seconds')));
  const covered = [oai('file-4', GIB, 'batch', 1, NOW + 10 * DAY)];
  const [clean, cleanDetail] = gradeExpiry(covered, NOW, 90);
  assert.equal(clean, 'expiry-covered');
  assert.ok(cleanDetail.includes('lifecycle'));
  assert.deepEqual(repairLines(clean), []);
  assert.equal(gradeExpiry([], NOW, 90)[0], 'expiry-none');
});

test('every repair is printed and none of them reclaims anything', () => {
  for (const state of ['quota-critical', 'quota-warning', 'purpose-dominates',
                       'file-near-cap', 'no-expiry-policy', 'quota-unknown']) {
    const lines = repairLines(state);
    assert.ok(lines.length, state);
    assert.ok(lines.every((l) => typeof l === 'string' && l.length));
  }
  assert.ok(repairLines('purpose-dominates').some((l) => l.includes('cannot be recovered')));
  assert.deepEqual(repairLines('quota-headroom'), []);
  assert.deepEqual(repairLines('expiry-covered'), []);
});

FAQ

Is there really no endpoint that tells me how much storage I am using?

There is not, on either provider, and that is why this is a paging script rather than a single GET. Nothing returns a quota, a consumed figure, a remaining figure or a percentage. The listing is the only surface that knows anything about your files, so the total has to be assembled from it by summing a size field over every page. That is also why the ceiling in the output is labelled documented rather than reported: it came from a docs page, no request confirms it, and OpenAI's own files documentation invites you to ask for a higher limit, which means some accounts are working against a number the default would get wrong.

Which container does the limit actually apply to?

Different ones, which is the trap. OpenAI documents 2.5 TB per project and states there is no organization-wide storage limit at all, so ten projects are ten independent ceilings and a project key can only see one of them. Anthropic documents 1 TB per organization while its Files API is workspace-scoped, so one workspace is a slice of the number that governs you and a clean run there proves less than it looks. The script prints which container it just measured and names the stores it did not look at.

Why not just delete the old files while the script is already looking at them?

Because a deleted file cannot be recovered, nothing in the metadata can tell an audit which files matter, and on OpenAI the documented behaviour of the delete call is to remove the file from every vector store it is attached to as well. A 400-day-old file with no expiry might be the corpus behind a fine-tuned model serving production traffic, and it looks exactly like a batch input from a job that finished last quarter. The script prints one deletion command per candidate and stops there.

The store is nowhere near the ceiling but uploads are still failing. What now?

Look at the per-file cap, which is the third finding the script prints and a completely separate limit: 512 MB on OpenAI, 500 MB on Anthropic, applied to one upload rather than to the store. A single oversized file is rejected while the store is 3% full, and the error reads similarly enough to a quota error that it eats the first hour of the investigation. The script grades the two independently and lists the largest files so you can see whether one of them is the actual problem.

How is this different from the note about vector store storage cost?

Different object, different failure, different repair. That note reads usage_bytes on vector stores, trends it across ninety days against retrieval volume, and prices the slope from a gibibyte_hours line item: its finding is money and its shape is a curve. This one reads the file store, where Anthropic states outright that Files API operations are free, and compares one number to a hard ceiling: the finding is not on the invoice at all, it is how many uploads you have left. A file can be in both populations at once, which is fine, because you would fix that in two different places.

Related field notes

Sources

Every figure in this note is traced to one of these. Prices are list rates and change — check them for your own region before acting.

Stuck on a tricky one?

If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.