Diagnostic LLM APIs
Priority Tier never covered the model you migrated to
The commitment was signed two years ago, and the whole point of it was the 529s. It worked: the overload errors stopped, the on-call rotation got quiet, and everybody moved on. Then the platform team migrated the main assistant to a newer model, which was the correct thing to do on every axis they measured, and nothing in the migration checklist mentioned tiers because service_tier was already set to auto and had been for two years. The 529s came back four months later and nobody connected the two events, because there is no error, no warning and no line on the invoice that says the tier stopped applying.
One paged GET with an Admin API key: GET /v1/organizations/usage_report/messages?starting_at={T-30d}&bucket_width=1d&limit=31&group_by[]=service_tier&group_by[]=model. Each result carries a service_tier of priority, standard or batch. Fold the tokens per model per tier and look for the models where priority never appears at all.
Then read the absence correctly, because there are two very different reasons for it. Priority Tier is not supported on Claude Opus 5, Claude Sonnet 5, Claude Mythos 5 or Claude Mythos Preview. A model on that list has no coverage to lose: service_tier: "auto" is accepted, the request succeeds, and it is served standard every time. That is not a setting anyone got wrong. It is a capability that does not exist for that model id.
The other reason is that the organization has no commitment at all — Priority Tier capacity is no longer available to purchase, so an org without an existing one will show zero priority tokens on every model. The script checks that first and says so, because reporting a per-model coverage gap in an org that has no priority capacity anywhere would be a fabricated finding.
No dollar figure appears in the output, and that is deliberate. Priority Tier costs are excluded from the Cost API entirely. The usage report is the only read-only source for this, and it reports tokens, not money.
The problem in plain words
Two independent traps sit behind one field, and the field reports success in both. service_tier: "auto" means "use the priority commitment if it applies, otherwise serve me standard". It is a graceful fallback, which is the right design and the reason nothing ever raises. The request is not rejected for being on an unsupported model. The response is not marked. The latency is a distribution, not an alarm.
The first trap is model coverage. Priority Tier runs on most Claude models and specifically not on Opus 5, Sonnet 5, Mythos 5 or Mythos Preview, so the single most routine action a platform team takes — moving to a newer, better model — is also the action that silently ends priority routing. Nothing in the migration diff touches tiers, because the tier was never in the diff.
The second is capacity. A commitment buys a fixed input and output tokens per minute; traffic past it falls back to standard automatically, and priority requests draw on the ordinary rate limits too, so one that would breach those is declined rather than served. A commitment that was sized for last year's traffic is a commitment that mostly is not applying, and the shape of that in the report is a small non-zero priority share rather than a clean zero.
Why it happens
This is a coverage question, not a configuration question, and they have different repairs. A published note already reads the tier you configured against the tier the invoice says served you, in both directions, and that comparison is the right one when a setting is wrong. It is the wrong one here, because nothing is set wrong: the request asks for auto, auto is valid, and the model simply is not on the list. The only repair is a model id or an acceptance that this workload runs standard, and the tier-mismatch note is where you go when the setting really is the problem.
An org-wide zero and a per-model zero look identical in one row and mean opposite things. If no model anywhere in the organization reports priority, the finding is not about any model — it is that there is no commitment, or none that ever applies, and capacity commitments cannot be bought any more. If some models report priority and one does not, that one model is uncovered. The script establishes the org-level fact before it grades a single model, because getting this backwards produces a confident report blaming a model for something the organization never had.
Priority spend is invisible to the cost report by design. The Cost API excludes Priority Tier costs, so there is no read-only way to put a number on what priority traffic cost or what standard fallback saved. A script that multiplied token counts by a published rate here would be inventing a figure and presenting it as a reading. This one prints tokens and shares, states that money is not available on this surface, and stops.
A small priority share is a capacity finding and reads differently from a zero. Zero means never eligible. Fifteen per cent means eligible and mostly over the committed tokens per minute, which is a sizing conversation rather than a model conversation, and which also means your overload protection is protecting about fifteen per cent of your traffic. The two states are graded separately and carry different repairs.
Burndown makes the committed capacity go further or less far than the raw token count suggests. Against a commitment, cache reads burn at 0.1x, five-minute cache writes at 1.25x, one-hour cache writes at 2.0x, and inference_geo: "us" at 1.1x on 4.6 and later. The script reports the raw tokens it read and names the multipliers rather than applying them, because it cannot see which of your traffic carried which of those attributes without regrouping the whole report.
The fix, as a flow
There is no failing request anywhere in this. service_tier auto is valid, the model is valid, and the fallback to standard is the documented behaviour rather than a fault. The only place the absence is visible is the usage report grouped by service_tier, because Priority costs are excluded from the cost report entirely, which is also why no dollar figure appears at the end of it.
How to fix it
Use an Admin API key, provisioned read-only
Everything under /v1/organizations/* rejects a workspace key outright, and an Admin key can be created read-only. This script issues one paged GET and nothing else.
Group the usage report by service_tier and model together
group_by[]=service_tier&group_by[]=model, bucket_width=1d, limit=31, starting_at floored to midnight UTC. Both dimensions matter: grouped by tier alone, one covered model hides every uncovered one behind an organization-wide average that looks fine.
Establish whether the organization has any priority traffic at all
Sum priority tokens across every model in the window. Zero everywhere means there is no commitment in force, and the correct output is one line saying so rather than a list of models. Capacity commitments are no longer sold, so this is a common and entirely legitimate state.
Grade each model against the documented exclusion list
Opus 5, Sonnet 5, Mythos 5 and Mythos Preview are documented as unsupported. A model on that list with zero priority tokens is a structural gap. A model not on the list with zero priority tokens is something else — a request-side standard_only, a workspace outside the commitment, or capacity that never had headroom — and the script labels it differently rather than guessing.
Print the coverage table, and refuse to price it
Per model: the priority, standard and batch shares, the tokens behind them, and the repair. No dollars, because Priority costs are excluded from the cost report and the number would have to be invented to appear.
How to check it worked
Re-run after a model change lands. A model that moves from a clean zero to a real priority share is now covered; one that stays at zero after the id changed is either still on the exclusion list or the commitment does not reach that workspace.
python3 anthropic_priority_tier_coverage.py --days 30
# org has priority traffic on 2 of 5 model(s), so a per-model zero is meaningful
# unsupported-model claude-opus-5 0% priority of 812.4M token(s). Documented as not supported by Priority Tier.
# repair: service_tier auto is accepted here and served standard every time. This is coverage, not configuration.
# partial-priority claude-haiku-4-5-20251001 14% priority of 240.1M token(s)
# repair: traffic past the committed tokens per minute falls back to standard. This is a sizing question.
# priority-covered claude-opus-4-5 91% priority of 45.9M token(s)
# 5 model(s) checked, 2 finding(s)
# no dollar figure: Priority Tier costs are excluded from the cost report
The full code
One paged GET and seven pure functions. The tier normaliser, which keeps an absent service_tier out of the standard bucket; the token weigher, which has to walk the nested cache_creation object; the fold; the exclusion-list matcher, written to match model families without catching claude-opus-4-5 on the substring opus-5; the org-level priority check that has to run before any model is graded; the per-model verdict; and the repair lines. Nothing in it converts a token into a dollar, because the surface that would let it does not report this traffic.
"""Find Claude models that never report Priority Tier service.
Read only. One paged GET against the messages usage report with an Admin API
key. Nothing is sent to /v1/messages and no request body is constructed.
The finding is coverage, not misconfiguration: Priority Tier is not supported on
every model id, so a migration to a newer model can end priority routing with no
error and no diff. The absence is only visible in the usage report grouped by
service_tier, because Priority Tier costs are excluded from the cost report.
No dollar figure is printed anywhere. There is no read-only source for the money
on this surface, and a number derived from a published rate would be a guess
wearing the clothes of a reading.
"""
import argparse
import datetime as dt
import logging
import os
import sys
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("anthropic_priority_tier_coverage")
API = "https://api.anthropic.com/v1"
VERSION = "2023-06-01"
PRIORITY = "priority"
STANDARD = "standard"
BATCH = "batch"
UNKNOWN = "unknown"
TIERS = (PRIORITY, STANDARD, BATCH, UNKNOWN)
# Documented as NOT supported by Priority Tier. Matched as a family fragment
# rather than an exact id, because ids carry date suffixes. The fragments are
# written with their leading hyphen so that "opus-5" cannot match
# "claude-opus-4-5", which is a supported model and would otherwise be
# condemned by a careless substring test.
UNSUPPORTED_FAMILIES = ("-opus-5", "-sonnet-5", "-mythos-5", "-mythos-preview")
# Burndown multipliers against a commitment. Named in the output, never applied:
# the script cannot see which tokens carried which attribute without regrouping
# the entire report, and applying an average would be worse than saying nothing.
BURNDOWN = ("cache reads 0.1x", "5-minute cache writes 1.25x",
"1-hour cache writes 2.0x", "inference_geo us 1.1x on 4.6+")
FINDINGS = ("unsupported-model", "uncovered-model", "partial-priority")
def tier(result):
"""Normalise the service_tier on one result row. Pure.
An absent or unrecognised value becomes "unknown" and never "standard".
Folding unclassified traffic into standard inflates the standard share,
which is the direction that makes a coverage gap look worse than it is.
"""
raw = str((result or {}).get("service_tier") or "").strip().lower()
return raw if raw in (PRIORITY, STANDARD, BATCH) else UNKNOWN
def weigh(result):
"""Total billed tokens on one result row. Pure.
cache_creation is an object rather than a scalar, so a reader that treats it
as an int silently drops every cached write from the weight and understates
the models that cache the most.
"""
row = result or {}
total = 0
for field in ("uncached_input_tokens", "cache_read_input_tokens",
"output_tokens"):
try:
total += int(row.get(field) or 0)
except (TypeError, ValueError):
pass
creation = row.get("cache_creation")
if isinstance(creation, dict):
for value in creation.values():
try:
total += int(value or 0)
except (TypeError, ValueError):
pass
return total
def fold(pages):
"""Sum tokens into {model: {tier: tokens}}. Pure."""
out = {}
for page in pages or []:
for bucket in (page or {}).get("data") or []:
for result in (bucket or {}).get("results") or []:
model = str((result or {}).get("model") or "all models")
row = out.setdefault(model, {t: 0 for t in TIERS})
row[tier(result)] += weigh(result)
return out
def is_unsupported(model):
"""Is this model id on the documented Priority Tier exclusion list? Pure.
Matched on the hyphenated family fragment. claude-opus-4-5 and
claude-haiku-4-5 are supported and must not match; claude-opus-5 and
claude-sonnet-5-20260101 must.
"""
name = "-" + str(model or "").strip().lower().lstrip("-")
return any(fragment in name for fragment in UNSUPPORTED_FAMILIES)
def org_has_priority(rows):
"""Does any model in the window report priority tokens? Pure.
Run before any model is graded. Priority Tier capacity can no longer be
bought, so an organization with no commitment reports zero everywhere, and
a per-model coverage verdict in that organization would be a finding about
nothing.
"""
return any(int((row or {}).get(PRIORITY) or 0) > 0
for row in (rows or {}).values())
def share(row, which):
"""One tier's share of a model's billed tokens. Pure. 0.0 when empty."""
data = row or {}
total = sum(int(data.get(t) or 0) for t in TIERS)
if total <= 0:
return 0.0
return int(data.get(which) or 0) / float(total)
def verdict(model, row, has_priority, min_tokens=1_000_000, thin=0.60):
"""Classify one model's tier coverage. Pure. Returns (state, detail)."""
data = row or {}
total = sum(int(data.get(t) or 0) for t in TIERS)
if total < min_tokens:
return ("low-volume",
"%d billed token(s) in the window, too few to conclude anything"
% total)
if not has_priority:
return ("no-priority-in-org",
"0%% priority of %.1fM token(s), and no model in this "
"organization reports priority either. That is an organization "
"without a capacity commitment, not a gap on this model."
% (total / 1e6))
got = share(data, PRIORITY)
if got <= 0:
if is_unsupported(model):
return ("unsupported-model",
"0%% priority of %.1fM token(s). Documented as not "
"supported by Priority Tier, so service_tier auto is "
"accepted here and served standard every time."
% (total / 1e6))
return ("uncovered-model",
"0%% priority of %.1fM token(s), and this id is not on the "
"documented exclusion list. Something else is keeping it off "
"the tier: standard_only on the request, a workspace outside "
"the commitment, or capacity that never had headroom."
% (total / 1e6))
if got < thin:
return ("partial-priority",
"%.0f%% priority of %.1fM token(s). Eligible, and mostly over "
"the committed tokens per minute, so the rest fell back to "
"standard." % (got * 100, total / 1e6))
return ("priority-covered",
"%.0f%% priority of %.1fM token(s)" % (got * 100, total / 1e6))
def repair_lines(state, model):
"""The repair for one classified model. Pure. Printed, never performed."""
if state == "unsupported-model":
return [
"this is coverage, not configuration: %s cannot be served on "
"Priority Tier at all, whatever service_tier says." % model,
"either move the latency-sensitive traffic to a covered model id, "
"or accept standard here and stop planning around a tier that "
"never applies to it.",
"standard_only is the way to deliberately preserve commitment "
"capacity for the models that can use it.",
]
if state == "uncovered-model":
return [
"check the request side for standard_only, and check that the "
"workspace sending this traffic is inside the commitment.",
"the exclusion list is not the explanation for %s, so the answer "
"is in your own configuration or in capacity." % model,
]
if state == "partial-priority":
return [
"the commitment is sized below this traffic. Requests past the "
"committed input and output tokens per minute fall back to "
"standard automatically, and one that would breach the ordinary "
"rate limits is declined rather than served.",
"burndown against the commitment is not one token per token: %s."
% ", ".join(BURNDOWN),
]
return []
def get(session, path, params):
r = session.get(API + path, params=params, timeout=60)
if r.status_code in (401, 403):
raise SystemExit("%d from Anthropic: /v1/organizations/* needs an Admin "
"API key (sk-ant-admin...), not a workspace key"
% r.status_code)
r.raise_for_status()
return r.json()
def pages(session, path, params):
"""Walk the paginated usage report."""
params = dict(params)
while True:
page = get(session, path, params)
yield page
if not page.get("has_more") or not page.get("next_page"):
return
params["page"] = page["next_page"]
def window_start(days):
"""Floor to midnight UTC: starting_at must sit on a bucket boundary."""
now = dt.datetime.now(dt.timezone.utc)
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
return (midnight - dt.timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--days", type=int, default=30,
help="days of daily buckets to read (default 30)")
ap.add_argument("--min-tokens", type=int, default=1_000_000,
help="billed tokens below which no claim is made")
ap.add_argument("--thin", type=float, default=0.60,
help="priority share below which coverage is called partial")
args = ap.parse_args()
admin = os.environ.get("ANTHROPIC_ADMIN_KEY")
if not admin:
log.error("set ANTHROPIC_ADMIN_KEY to an Admin API key (sk-ant-admin...); "
"a workspace key cannot read /v1/organizations/*")
return 2
s = requests.Session()
s.headers.update({"x-api-key": admin, "anthropic-version": VERSION})
rows = fold(pages(s, "/organizations/usage_report/messages",
{"starting_at": window_start(args.days),
"bucket_width": "1d", "limit": min(args.days + 1, 31),
"group_by[]": ["service_tier", "model"]}))
if not rows:
log.info("no usage in the last %d day(s)", args.days)
return 0
has_priority = org_has_priority(rows)
covered = sum(1 for row in rows.values() if int(row.get(PRIORITY) or 0) > 0)
if has_priority:
log.info("org has priority traffic on %d of %d model(s), so a per-model "
"zero is meaningful", covered, len(rows))
else:
log.warning("no model in this organization reported any priority "
"token(s) in the window. Capacity commitments are no longer "
"available to purchase, so this is an organization without "
"one rather than a gap on any single model.")
bad = 0
for model in sorted(rows, key=lambda m: -sum(rows[m].values())):
state, detail = verdict(model, rows[model], has_priority,
args.min_tokens, args.thin)
line = "%-20s %-26s %s" % (state, model, detail)
if state in FINDINGS:
bad += 1
log.warning(line)
for repair in repair_lines(state, model):
log.warning(" repair: %s", repair)
else:
log.info(line)
log.info("%d model(s) checked, %d finding(s)", len(rows), bad)
log.info("no dollar figure: Priority Tier costs are excluded from the cost "
"report, so tokens are the only read-only reading available here")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
/**
* Find Claude models that never report Priority Tier service.
*
* Read only. One paged GET against the messages usage report with an Admin API
* key. Nothing is sent to /v1/messages.
*
* The finding is coverage, not misconfiguration, and no dollar figure is
* printed: Priority Tier costs are excluded from the cost report, so there is
* no read-only source for the money on this surface.
*/
const API = 'https://api.anthropic.com/v1';
const VERSION = '2023-06-01';
const PRIORITY = 'priority';
const STANDARD = 'standard';
const BATCH = 'batch';
const UNKNOWN = 'unknown';
const TIERS = [PRIORITY, STANDARD, BATCH, UNKNOWN];
// Documented as NOT supported by Priority Tier. Family fragments carry their
// leading hyphen so that "-opus-5" cannot match claude-opus-4-5.
const UNSUPPORTED_FAMILIES = ['-opus-5', '-sonnet-5', '-mythos-5', '-mythos-preview'];
const BURNDOWN = ['cache reads 0.1x', '5-minute cache writes 1.25x',
'1-hour cache writes 2.0x', 'inference_geo us 1.1x on 4.6+'];
const FINDINGS = new Set(['unsupported-model', 'uncovered-model', 'partial-priority']);
/** Normalise the service_tier on one result row. Pure. Absent is never standard. */
export function tier(result) {
const raw = String(result?.service_tier ?? '').trim().toLowerCase();
return [PRIORITY, STANDARD, BATCH].includes(raw) ? raw : UNKNOWN;
}
/** Total billed tokens on one result row. Pure. cache_creation is an object. */
export function weigh(result) {
const row = result ?? {};
let total = 0;
for (const field of ['uncached_input_tokens', 'cache_read_input_tokens',
'output_tokens']) {
const n = Number(row[field] ?? 0);
if (Number.isFinite(n)) total += Math.trunc(n);
}
if (row.cache_creation && typeof row.cache_creation === 'object') {
for (const value of Object.values(row.cache_creation)) {
const n = Number(value ?? 0);
if (Number.isFinite(n)) total += Math.trunc(n);
}
}
return total;
}
/** Sum tokens into { model: { tier: tokens } }. Pure. */
export function fold(pages) {
const out = {};
for (const page of pages ?? []) {
for (const bucket of page?.data ?? []) {
for (const result of bucket?.results ?? []) {
const model = String(result?.model ?? 'all models');
if (!out[model]) {
out[model] = Object.fromEntries(TIERS.map((t) => [t, 0]));
}
out[model][tier(result)] += weigh(result);
}
}
}
return out;
}
/** Is this model id on the documented Priority Tier exclusion list? Pure. */
export function isUnsupported(model) {
const name = `-${String(model ?? '').trim().toLowerCase().replace(/^-+/, '')}`;
return UNSUPPORTED_FAMILIES.some((fragment) => name.includes(fragment));
}
/**
* Does any model in the window report priority tokens? Pure.
* Run before any model is graded: an org with no commitment reports zero
* everywhere, and grading a model against that is a finding about nothing.
*/
export function orgHasPriority(rows) {
return Object.values(rows ?? {}).some((row) => Number(row?.[PRIORITY] ?? 0) > 0);
}
/** One tier's share of a model's billed tokens. Pure. 0 when empty. */
export function share(row, which) {
const data = row ?? {};
const total = TIERS.reduce((sum, t) => sum + Number(data[t] ?? 0), 0);
if (total <= 0) return 0;
return Number(data[which] ?? 0) / total;
}
/** Classify one model's tier coverage. Pure. Returns [state, detail]. */
export function verdict(model, row, hasPriority, minTokens = 1_000_000, thin = 0.6) {
const data = row ?? {};
const total = TIERS.reduce((sum, t) => sum + Number(data[t] ?? 0), 0);
if (total < minTokens) {
return ['low-volume',
`${total} billed token(s) in the window, too few to conclude anything`];
}
if (!hasPriority) {
return ['no-priority-in-org',
`0% priority of ${(total / 1e6).toFixed(1)}M token(s), and no model in ` +
'this organization reports priority either. That is an organization ' +
'without a capacity commitment, not a gap on this model.'];
}
const got = share(data, PRIORITY);
if (got <= 0) {
if (isUnsupported(model)) {
return ['unsupported-model',
`0% priority of ${(total / 1e6).toFixed(1)}M token(s). Documented as ` +
'not supported by Priority Tier, so service_tier auto is accepted ' +
'here and served standard every time.'];
}
return ['uncovered-model',
`0% priority of ${(total / 1e6).toFixed(1)}M token(s), and this id is ` +
'not on the documented exclusion list. Something else is keeping it off ' +
'the tier: standard_only on the request, a workspace outside the ' +
'commitment, or capacity that never had headroom.'];
}
if (got < thin) {
return ['partial-priority',
`${(got * 100).toFixed(0)}% priority of ${(total / 1e6).toFixed(1)}M ` +
'token(s). Eligible, and mostly over the committed tokens per minute, ' +
'so the rest fell back to standard.'];
}
return ['priority-covered',
`${(got * 100).toFixed(0)}% priority of ${(total / 1e6).toFixed(1)}M token(s)`];
}
/** The repair for one classified model. Pure. Printed, never performed. */
export function repairLines(state, model) {
if (state === 'unsupported-model') {
return [
`this is coverage, not configuration: ${model} cannot be served on ` +
'Priority Tier at all, whatever service_tier says.',
'either move the latency-sensitive traffic to a covered model id, or ' +
'accept standard here and stop planning around a tier that never ' +
'applies to it.',
'standard_only is the way to deliberately preserve commitment capacity ' +
'for the models that can use it.',
];
}
if (state === 'uncovered-model') {
return [
'check the request side for standard_only, and check that the workspace ' +
'sending this traffic is inside the commitment.',
`the exclusion list is not the explanation for ${model}, so the answer ` +
'is in your own configuration or in capacity.',
];
}
if (state === 'partial-priority') {
return [
'the commitment is sized below this traffic. Requests past the committed ' +
'input and output tokens per minute fall back to standard automatically, ' +
'and one that would breach the ordinary rate limits is declined rather ' +
'than served.',
`burndown against the commitment is not one token per token: ${BURNDOWN.join(', ')}.`,
];
}
return [];
}
/** Floor to midnight UTC: starting_at must sit on a bucket boundary. */
export function windowStart(days, now = new Date()) {
const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(),
now.getUTCDate()));
midnight.setUTCDate(midnight.getUTCDate() - days);
return `${midnight.toISOString().slice(0, 19)}Z`;
}
async function get(key, path, params) {
const url = new URL(API + path);
for (const [k, v] of params) url.searchParams.append(k, v);
const res = await fetch(url, {
headers: { 'x-api-key': key, 'anthropic-version': VERSION },
});
if (res.status === 401 || res.status === 403) {
throw new Error(`${res.status} from Anthropic: /v1/organizations/* needs ` +
'an Admin API key (sk-ant-admin...), not a workspace key');
}
if (!res.ok) throw new Error(`${res.status} from ${path}`);
return res.json();
}
async function main() {
const admin = process.env.ANTHROPIC_ADMIN_KEY;
if (!admin) {
console.error('set ANTHROPIC_ADMIN_KEY to an Admin API key (sk-ant-admin...); ' +
'a workspace key cannot read /v1/organizations/*');
process.exitCode = 2;
return;
}
const days = Number(process.env.DAYS ?? 30);
const minTokens = Number(process.env.MIN_TOKENS ?? 1_000_000);
const base = [['starting_at', windowStart(days)],
['bucket_width', '1d'],
['limit', String(Math.min(days + 1, 31))],
['group_by[]', 'service_tier'],
['group_by[]', 'model']];
const collected = [];
let params = base;
for (;;) {
const page = await get(admin, '/organizations/usage_report/messages', params);
collected.push(page);
if (!page?.has_more || !page?.next_page) break;
params = [...base, ['page', page.next_page]];
}
const rows = fold(collected);
const models = Object.keys(rows);
if (models.length === 0) {
console.log(`no usage in the last ${days} day(s)`);
return;
}
const hasPriority = orgHasPriority(rows);
const covered = models.filter((m) => Number(rows[m][PRIORITY] ?? 0) > 0).length;
if (hasPriority) {
console.log(`org has priority traffic on ${covered} of ${models.length} ` +
'model(s), so a per-model zero is meaningful');
} else {
console.warn('no model in this organization reported any priority token(s) ' +
'in the window. Capacity commitments are no longer available ' +
'to purchase, so this is an organization without one rather ' +
'than a gap on any single model.');
}
const weight = (m) => TIERS.reduce((sum, t) => sum + Number(rows[m][t] ?? 0), 0);
let bad = 0;
for (const model of models.sort((a, b) => weight(b) - weight(a))) {
const [state, detail] = verdict(model, rows[model], hasPriority, minTokens);
const line = `${state.padEnd(20)} ${model.padEnd(26)} ${detail}`;
if (FINDINGS.has(state)) {
bad += 1;
console.warn(line);
for (const repair of repairLines(state, model)) {
console.warn(` repair: ${repair}`);
}
} else {
console.log(line);
}
}
console.log(`${models.length} model(s) checked, ${bad} finding(s)`);
console.log('no dollar figure: Priority Tier costs are excluded from the ' +
'cost report, so tokens are the only read-only reading here');
process.exitCode = bad ? 1 : 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}
Add a test
Two tests carry the note and they are the same data read under different org-level facts. A corpus where one model reports priority and claude-opus-5 reports none has to come back as unsupported-model; the identical corpus with the priority row removed has to come back as no-priority-in-org on every model, because an organization without a commitment has no per-model gap to report. After that: the exclusion matcher, which has to catch claude-sonnet-5-20260101 and leave claude-opus-4-5 and claude-haiku-4-5-20251001 alone; the partial share that is a sizing finding rather than a coverage one; a nested cache_creation object that a scalar reader would drop; and an absent service_tier, which must not land in the standard bucket.
from anthropic_priority_tier_coverage import (fold, is_unsupported,
org_has_priority, repair_lines,
share, tier, verdict, weigh)
def result(model, service_tier, tokens):
return {"model": model, "service_tier": service_tier,
"uncached_input_tokens": tokens}
def page(results):
return {"data": [{"results": results}], "has_more": False}
COVERED_ORG = [page([
result("claude-opus-5", "standard", 812_400_000),
result("claude-opus-4-5", "priority", 41_800_000),
result("claude-opus-4-5", "standard", 4_100_000),
])]
def test_a_model_that_never_reports_priority_has_no_coverage():
# The note in one assertion. The org clearly has a commitment, because
# another model is being served on it, so this model's clean zero is a
# fact about the model rather than about the organization.
rows = fold(COVERED_ORG)
assert org_has_priority(rows) is True
state, detail = verdict("claude-opus-5", rows["claude-opus-5"], True)
assert state == "unsupported-model"
assert "not supported by Priority Tier" in detail
assert verdict("claude-opus-4-5", rows["claude-opus-4-5"], True)[0] == \
"priority-covered"
assert any("coverage, not configuration" in line
for line in repair_lines(state, "claude-opus-5"))
def test_an_org_with_no_commitment_is_not_a_per_model_finding():
# Identical traffic with the priority row removed. Nothing about the model
# changed; the correct verdict did, because there is no commitment anywhere
# and Priority capacity can no longer be bought.
rows = fold([page([
result("claude-opus-5", "standard", 812_400_000),
result("claude-opus-4-5", "standard", 45_900_000),
])])
assert org_has_priority(rows) is False
for model in rows:
state, detail = verdict(model, rows[model], False)
assert state == "no-priority-in-org"
assert "without a capacity commitment" in detail
assert repair_lines(state, model) == []
def test_the_exclusion_list_matches_families_and_not_neighbours():
assert is_unsupported("claude-opus-5") is True
assert is_unsupported("claude-sonnet-5-20260101") is True
assert is_unsupported("claude-mythos-5") is True
assert is_unsupported("claude-mythos-preview") is True
# The ones a careless substring test destroys.
assert is_unsupported("claude-opus-4-5") is False
assert is_unsupported("claude-haiku-4-5-20251001") is False
assert is_unsupported("claude-sonnet-4-6") is False
assert is_unsupported("claude-fable-5") is False
assert is_unsupported(None) is False
def test_a_model_off_the_list_with_zero_priority_is_a_different_finding():
rows = fold([page([
result("claude-haiku-4-5-20251001", "standard", 240_000_000),
result("claude-opus-4-5", "priority", 40_000_000),
])])
state, detail = verdict("claude-haiku-4-5-20251001",
rows["claude-haiku-4-5-20251001"], True)
assert state == "uncovered-model"
assert "not on the documented exclusion list" in detail
def test_a_thin_priority_share_is_a_sizing_finding():
rows = fold([page([
result("claude-haiku-4-5-20251001", "priority", 14_000_000),
result("claude-haiku-4-5-20251001", "standard", 86_000_000),
])])
state, detail = verdict("claude-haiku-4-5-20251001",
rows["claude-haiku-4-5-20251001"], True)
assert state == "partial-priority"
assert "14% priority" in detail
assert any("burndown" in line for line in
repair_lines(state, "claude-haiku-4-5-20251001"))
def test_cache_creation_is_an_object_and_all_of_it_counts():
row = {"uncached_input_tokens": 100, "cache_read_input_tokens": 10,
"output_tokens": 5,
"cache_creation": {"ephemeral_5m_input_tokens": 40,
"ephemeral_1h_input_tokens": 20}}
assert weigh(row) == 175
assert weigh({"uncached_input_tokens": "not a number"}) == 0
assert weigh({"cache_creation": 12}) == 0
assert weigh(None) == 0
def test_an_absent_service_tier_never_lands_in_standard():
assert tier({"service_tier": "priority"}) == "priority"
assert tier({"service_tier": "BATCH"}) == "batch"
assert tier({}) == "unknown"
assert tier({"service_tier": "flex"}) == "unknown"
rows = fold([page([result("claude-opus-5", None, 5_000_000)])])
assert rows["claude-opus-5"]["standard"] == 0
assert rows["claude-opus-5"]["unknown"] == 5_000_000
assert share(rows["claude-opus-5"], "standard") == 0.0
def test_too_little_traffic_is_never_a_verdict():
rows = fold([page([result("claude-opus-5", "standard", 900)])])
state, detail = verdict("claude-opus-5", rows["claude-opus-5"], True)
assert state == "low-volume"
assert "too few to conclude" in detail
assert fold([]) == {} and fold(None) == {}
assert org_has_priority({}) is False
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { fold, isUnsupported, orgHasPriority, repairLines, share, tier,
verdict, weigh, windowStart }
from './anthropic-priority-tier-coverage.mjs';
const result = (model, serviceTier, tokens) =>
({ model, service_tier: serviceTier, uncached_input_tokens: tokens });
const page = (results) => ({ data: [{ results }], has_more: false });
const COVERED_ORG = [page([
result('claude-opus-5', 'standard', 812_400_000),
result('claude-opus-4-5', 'priority', 41_800_000),
result('claude-opus-4-5', 'standard', 4_100_000),
])];
test('a model that never reports priority has no coverage', () => {
const rows = fold(COVERED_ORG);
assert.equal(orgHasPriority(rows), true);
const [state, detail] = verdict('claude-opus-5', rows['claude-opus-5'], true);
assert.equal(state, 'unsupported-model');
assert.match(detail, /not supported by Priority Tier/);
assert.equal(verdict('claude-opus-4-5', rows['claude-opus-4-5'], true)[0],
'priority-covered');
assert.ok(repairLines(state, 'claude-opus-5')
.some((line) => line.includes('coverage, not configuration')));
});
test('an org with no commitment is not a per-model finding', () => {
const rows = fold([page([
result('claude-opus-5', 'standard', 812_400_000),
result('claude-opus-4-5', 'standard', 45_900_000),
])]);
assert.equal(orgHasPriority(rows), false);
for (const model of Object.keys(rows)) {
const [state, detail] = verdict(model, rows[model], false);
assert.equal(state, 'no-priority-in-org');
assert.match(detail, /without a capacity commitment/);
assert.deepEqual(repairLines(state, model), []);
}
});
test('the exclusion list matches families and not neighbours', () => {
assert.equal(isUnsupported('claude-opus-5'), true);
assert.equal(isUnsupported('claude-sonnet-5-20260101'), true);
assert.equal(isUnsupported('claude-mythos-5'), true);
assert.equal(isUnsupported('claude-mythos-preview'), true);
assert.equal(isUnsupported('claude-opus-4-5'), false);
assert.equal(isUnsupported('claude-haiku-4-5-20251001'), false);
assert.equal(isUnsupported('claude-sonnet-4-6'), false);
assert.equal(isUnsupported('claude-fable-5'), false);
assert.equal(isUnsupported(null), false);
});
test('a model off the list with zero priority is a different finding', () => {
const rows = fold([page([
result('claude-haiku-4-5-20251001', 'standard', 240_000_000),
result('claude-opus-4-5', 'priority', 40_000_000),
])]);
const [state, detail] = verdict('claude-haiku-4-5-20251001',
rows['claude-haiku-4-5-20251001'], true);
assert.equal(state, 'uncovered-model');
assert.match(detail, /not on the documented exclusion list/);
});
test('a thin priority share is a sizing finding', () => {
const rows = fold([page([
result('claude-haiku-4-5-20251001', 'priority', 14_000_000),
result('claude-haiku-4-5-20251001', 'standard', 86_000_000),
])]);
const [state, detail] = verdict('claude-haiku-4-5-20251001',
rows['claude-haiku-4-5-20251001'], true);
assert.equal(state, 'partial-priority');
assert.match(detail, /14% priority/);
assert.ok(repairLines(state, 'claude-haiku-4-5-20251001')
.some((line) => line.includes('burndown')));
});
test('cache_creation is an object and all of it counts', () => {
assert.equal(weigh({ uncached_input_tokens: 100, cache_read_input_tokens: 10,
output_tokens: 5,
cache_creation: { ephemeral_5m_input_tokens: 40,
ephemeral_1h_input_tokens: 20 } }), 175);
assert.equal(weigh({ uncached_input_tokens: 'not a number' }), 0);
assert.equal(weigh({ cache_creation: 12 }), 0);
assert.equal(weigh(null), 0);
});
test('an absent service_tier never lands in standard', () => {
assert.equal(tier({ service_tier: 'priority' }), 'priority');
assert.equal(tier({ service_tier: 'BATCH' }), 'batch');
assert.equal(tier({}), 'unknown');
assert.equal(tier({ service_tier: 'flex' }), 'unknown');
const rows = fold([page([result('claude-opus-5', null, 5_000_000)])]);
assert.equal(rows['claude-opus-5'].standard, 0);
assert.equal(rows['claude-opus-5'].unknown, 5_000_000);
assert.equal(share(rows['claude-opus-5'], 'standard'), 0);
});
test('too little traffic is never a verdict', () => {
const rows = fold([page([result('claude-opus-5', 'standard', 900)])]);
const [state, detail] = verdict('claude-opus-5', rows['claude-opus-5'], true);
assert.equal(state, 'low-volume');
assert.match(detail, /too few to conclude/);
assert.deepEqual(fold([]), {});
assert.deepEqual(fold(null), {});
assert.equal(orgHasPriority({}), false);
});
test('the window start is floored to midnight utc', () => {
assert.equal(windowStart(30, new Date('2026-08-31T17:45:12Z')),
'2026-08-01T00:00:00Z');
assert.equal(windowStart(0, new Date('2026-08-31T17:45:12Z')),
'2026-08-31T00:00:00Z');
});
FAQ
Which Claude models does Priority Tier not support?
Claude Opus 5, Claude Sonnet 5, Claude Mythos 5 and Claude Mythos Preview. Everything else that is generally available is covered. The practical consequence is that the most routine platform action there is, moving to a newer and better model, is also the action that ends priority routing, and it does so with no error, no warning and no diff to review, because service_tier is set once and never mentioned in a migration.
Why does the script not tell me what Priority Tier is costing me?
Because there is no read-only source for it. Priority Tier costs are excluded from the Cost API entirely, so a number in that output would have to be produced by multiplying token counts from the usage report by a rate copied off a pricing page. That is a guess with a decimal point on it. The script prints tokens and shares, says plainly that money is not available on this surface, and leaves the arithmetic to you if you want it.
Can I just buy more priority capacity?
Not any more. Priority Tier capacity commitments are no longer available for purchase, so only organizations with an existing commitment have any at all. That is why the script establishes whether the organization has any priority traffic before it grades a single model: in an org without a commitment, every model reports zero, and calling that a coverage gap on one model would be inventing a fault.
My priority share is fifteen per cent rather than zero. Is that broken?
No, it is undersized. A commitment buys a fixed number of input and output tokens per minute; traffic beyond that falls back to standard automatically rather than failing, and a priority request that would breach your ordinary rate limits is declined rather than served. Fifteen per cent means the tier is working and covering fifteen per cent of your traffic, which is worth knowing before the next overload incident is blamed on the provider.
Does the burndown really not match the token count?
It does not. Against a commitment, cache reads burn at 0.1x, five-minute cache writes at 1.25x, one-hour cache writes at 2.0x, and inference_geo set to us adds 1.1x on 4.6 and later models. The script names these and does not apply them, because it cannot see which of your tokens carried which attribute without regrouping the whole report, and an averaged multiplier would be a worse number than none.
Related field notes
- The tier you asked for against the tier the invoice says served you
- The overload losses a priority commitment was bought to prevent
- Which of the three token limiters actually emptied
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.
- Service tiers — Claude Docs
- Get messages usage report — Claude Admin API
- Usage and Cost API — Claude Docs
- Rate limits — Claude Docs
If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.