Tutorial: Pomiar zużycia tokenów na żądanie i egzekwowanie budżetu miesięcznego

Management Summary
Miesięczny budżet AI ogranicza wydatki aplikacji na zapytania do modeli i dodatkowe usługi. Sama ilość tekstu nie wystarcza, ponieważ modele, zachowana historia i narzędzia mają różne opłaty. Rezerwacja możliwego kosztu przed każdym zapytaniem zapobiega wielokrotnemu wydaniu tej samej pozostałej kwoty przez równoległe zadania. Faktyczne zużycie zastępuje później rezerwację. Ułatwia to planowanie pracy automatycznych asystentów, pod warunkiem objęcia wszystkich płatnych działań.
Spis treści
Użyteczny budżet API łączy trzy zapisy: rezerwację przed żądaniem, zmierzone zużycie po odpowiedzi i późniejsze uzgodnienie z opłatami dostawcy. Sama suma tokenów nie wystarcza. Dostawca, model, wariant usługi, klasa pamięci podręcznej i dodatkowe narzędzia mogą zmieniać cenę.
1. Znaczenie schematu zużycia
W Anthropic Messages input_tokens oznacza wejście nieobjęte pamięcią podręczną. Utworzenie cache i odczyt cache są osobnymi klasami wejścia; zapis może dodatkowo rozróżniać czas ważności. W OpenAI Responses input_tokens oznacza całe wejście, obejmujące odczyty cache i obsługiwane klasy zapisu. Dodanie tych podklas do sumy policzyłoby wejście dwukrotnie. To różne struktury rozliczeniowe, a nie tylko inne nazwy.
Poniższy fikcyjny, ujednolicony rekord używa czterech rozłącznych klas. Stawki w USD za milion tokenów służą obliczeniu i nie są aktualną ofertą konkretnego modelu. Rzeczywisty adapter musi obsługiwać wybrany endpoint oraz wszystkie właściwe czasy ważności cache.
from decimal import Decimal, ROUND_CEILING
# Fictional normalized classes and example USD rates per million.
counts = {"input": 12400, "write_5m": 8200, "read": 96000, "output": 1850}
rates = {k: Decimal(v) for k, v in {
"input": "3.00", "write_5m": "3.75", "read": "0.30", "output": "15.00"
}.items()}
cost_usd = sum(Decimal(counts[k]) * rates[k] for k in counts) / Decimal(1_000_000)
actual_u = int((cost_usd * 1_000_000).to_integral_value(rounding=ROUND_CEILING))
print(cost_usd, actual_u) # 0.1245, 124500
Dokładne składniki wynoszą 0,0372, 0,03075, 0,0288 i 0,02775 USD, razem 0,1245 USD. Wcześniejsze zaokrąglenie każdej wyświetlanej części może zmienić widoczną sumę. Arytmetyka dziesiętna zachowuje obliczenie; rejestr rezerwacji poniżej używa całkowitych mikro-USD, czyli 1 000 000 jednostek na USD. Zaokrąglenie w górę do tej jednostki jest lokalną regułą budżetową, nie zasadą fakturowania dostawcy.

2. Zliczanie przed wysłaniem i zapis po zakończeniu
Endpoint zliczania Anthropic jest bezpłatny w ramach limitów częstotliwości i daje oszacowanie bez logiki cache. Nie przyjmuje wszystkich wejść obsługiwanych przez Messages: aktualna dokumentacja wyklucza większość narzędzi serwerowych, konektor MCP oraz bloki obrazów i dokumentów ze źródłami URL lub plikowymi. Znaczenie mają model i pełna obsługiwana struktura żądania. Tokenizer samego tekstu nie jest uniwersalnym licznikiem narzędzi, obrazów i oprawy komunikatów.
Zużycie w strumieniu nie pojawia się zawsze dopiero w ostatnim zdarzeniu. Anthropic przekazuje początkowe dane w message_start i skumulowane aktualizacje w message_delta. Dodawanie wszystkich skumulowanych wartości zawyżyłoby wynik. Kolejne sumy 10 i 20 oznaczają na przykład łącznie 20 tokenów do tej chwili, a nie 30. Obsługiwana funkcja SDK zbierająca zakończoną wiadomość upraszcza zapis. Przerwany strumień lub błąd połączenia może pozostawić niepełne dane. Brak informacji pozostaje niewiadomą, a nie zerem.
Trwały rejestr audytowy powinien łączyć operację logiczną, unikalne ID próby, dostępne identyfikatory dostawcy, czasy, funkcję, dostawcę i model, wersję cennika, rezerwację i surowe zużycie. Prompty i klucze API nie są do tego potrzebne. Ponowienie jest osobną próbą z własną rezerwacją możliwych kosztów. Timeout nie dowodzi ani naliczenia opłaty, ani zatrzymania przetwarzania; nagłówki odpowiedzi mogły w ogóle nie dotrzeć.
3. Atomowa rezerwacja pełnej górnej granicy
Górna granica musi obejmować wejście, maksymalne właściwe wyjście, możliwe koszty zapisu cache oraz ograniczone koszty narzędzi i pętli. Sam limit wyjścia nie wystarcza. Nieznane ceny, nieograniczone pętle narzędziowe lub nieudany zapis rejestru muszą blokować wysyłkę. Obliczenie granicy nie powinno zakładać trafienia w cache.
Wykonywalny ledger.py pokazuje kontrolę dopuszczania żądań dla jednej aplikacji, jednej waluty i miesięcy kalendarzowych UTC. Wszystkie procesy muszą korzystać z tej samej lokalnej bazy SQLite. To nie baza dla wielu hostów ani kompletna integracja rozliczeniowa. Walidator ogranicza każdą kwotę do miliona USD wyrażonych w mikrojednostkach.
Przed pierwszą rezerwacją initialise(database_path) tworzy tabele. Zużycie wyjścia może obejmować niewidoczne tokeny rozumowania lub formatowania; długość widocznej odpowiedzi nie jest licznikiem wyjścia.
import sqlite3
from datetime import datetime, timezone
class BudgetExceeded(Exception):
pass
def units(value):
if type(value) is not int or not 0 <= value <= 10**12:
raise ValueError("Expected non-negative integer micro-USD, at most 10^12")
return value
def connect(path):
db = sqlite3.connect(path, timeout=10, isolation_level=None)
db.execute("PRAGMA foreign_keys=ON")
return db
def initialise(path):
db = connect(path)
try:
db.executescript("""
CREATE TABLE IF NOT EXISTS budgets(
month TEXT PRIMARY KEY, limit_u INTEGER NOT NULL,
breached INTEGER NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS calls(
attempt_id TEXT PRIMARY KEY,
month TEXT NOT NULL REFERENCES budgets(month),
reserved_u INTEGER NOT NULL, actual_u INTEGER,
state TEXT NOT NULL CHECK(state IN ('pending','settled')));
""")
finally:
db.close()
def reserve(path, attempt_id, upper_u, limit_u, now=None):
upper_u, limit_u = units(upper_u), units(limit_u)
if not isinstance(attempt_id, str) or not attempt_id:
raise ValueError("Stable, nonempty attempt ID required")
now = datetime.now(timezone.utc) if now is None else now
if now.tzinfo is None or now.utcoffset() is None:
raise ValueError("Timezone-aware timestamp required")
month = now.astimezone(timezone.utc).strftime("%Y-%m")
db = connect(path)
try:
db.execute("BEGIN IMMEDIATE")
if db.execute("SELECT 1 FROM calls WHERE attempt_id=?",
(attempt_id,)).fetchone():
db.commit()
return False # No second permission to send.
db.execute("INSERT OR IGNORE INTO budgets(month,limit_u) VALUES (?,?)",
(month, limit_u))
stored_limit, breached = db.execute(
"SELECT limit_u,breached FROM budgets WHERE month=?", (month,)
).fetchone()
if stored_limit != limit_u:
raise ValueError("Conflicting limit for this month")
used = db.execute(
"SELECT COALESCE(SUM(COALESCE(actual_u,reserved_u)),0) "
"FROM calls WHERE month=?", (month,)
).fetchone()[0]
if breached or used + upper_u > limit_u:
raise BudgetExceeded(month)
db.execute("INSERT INTO calls VALUES (?,?,?,?,?)",
(attempt_id, month, upper_u, None, "pending"))
db.commit()
return True
except BaseException:
db.rollback()
raise
finally:
db.close()
def settle(path, attempt_id, actual_u):
actual_u = units(actual_u)
db = connect(path)
try:
db.execute("BEGIN IMMEDIATE")
row = db.execute(
"SELECT month,reserved_u,actual_u,state FROM calls WHERE attempt_id=?",
(attempt_id,)
).fetchone()
if row is None:
raise ValueError("Unknown attempt")
month, reserved, old_actual, state = row
if state == "settled":
if old_actual != actual_u:
raise ValueError("Conflicting settlement")
db.commit()
return
db.execute("UPDATE calls SET actual_u=?,state='settled' WHERE attempt_id=?",
(actual_u, attempt_id))
if actual_u > reserved:
db.execute("UPDATE budgets SET breached=1 WHERE month=?", (month,))
db.commit()
except BaseException:
db.rollback()
raise
finally:
db.close()
BEGIN IMMEDIATE rozpoczyna transakcję zapisu przed odczytem sumy. Dzięki temu dwa równoległe wywołania nie mogą zarezerwować tej samej pozostałej części budżetu. Rezerwacja jest zatwierdzana przed wywołaniem sieciowym; transakcja nie pozostaje otwarta podczas oczekiwania na API. Tylko prawdziwy wynik reserve() dopuszcza wysyłkę. False oznacza istniejącą próbę, a nie ponowną zgodę na wysłanie. Błąd blokady lub bazy musi zatrzymać wywołanie.
PSEUDOCODE
validate provider, model, pricing snapshot and complete cost bound
persist a unique attempt ID and audit context
if reserve(database, attempt_id, upper_bound_u, monthly_limit_u) is True:
send exactly one provider attempt outside the database transaction
on complete usage:
persist the original usage and provider identifiers
calculate cost with the recorded pricing snapshot
settle(database, attempt_id, actual_cost_u)
on unknown outcome or unknown cost:
keep the reservation pending and investigate
Zapisany limit pozostaje stały w danym miesiącu. Późniejsze oznaczenie przekroczenia nie anuluje już dopuszczonych żądań.
Schemat jest pseudokodem adaptera dostawcy: wymaga sprawdzonej wersji cennika i pełnej górnej granicy. Wykonywalny kod powyżej nie wywołuje API. Rozliczenie zapisuje się raz; identyczne ponowienie jest akceptowane, sprzeczne kwoty są odrzucane. Gdy koszt przekracza rezerwację, nadwyżka zostaje zapisana, a dalsze dopuszczenia w tym miesiącu zablokowane. Nie cofa to już poniesionych kosztów.
4. Widoczne próby z nieznanym wynikiem
Awaria po rezerwacji, ale przed wysłaniem może blokować niewykorzystany budżet. Timeout po wysłaniu może pozostawić nieustalony realny koszt. Automatyczne wygaszanie obu przypadków otworzyłoby budżet bez dowodu. Odzyskanie wymaga rejestru i informacji dostawcy; tylko próby potwierdzone jako bezpłatne powinny być rozliczane zerem.
Miesiąc jest przypisywany przy rezerwacji według UTC. Późniejsze rozliczenie pozostaje w tym miesiącu. To reguła wewnętrzna, nie twierdzenie o dacie faktury. Zgłoszenie, wykonanie, lokalne rozliczenie i okres dostawcy powinny pozostać odrębne przy zadaniach asynchronicznych i granicach miesięcy.
Pytania i odpowiedzi
Czy faktura dostawcy na pewno pozostanie poniżej budżetu?
Nie. Górna granica, adapter cen i objęcie wszystkich płatnych operacji muszą być poprawne. Inne klucze API, pominięte opłaty narzędziowe, umowy, podatki i późniejsze korekty mogą pozostawać poza lokalnym rejestrem. Limity dostawcy mogą dodatkowo chronić, jeśli są dostępne, lecz znaczenie ma ich udokumentowany zakres.
Czy uzgodnienie zawsze dopasuje pojedyncze żądania?
Nie. API Usage i Cost Anthropic udostępniają zagregowane przedziały czasu i wymiary grupowania. Nie można zakładać eksportu każdego żądania osobno. Raport kosztów również ma udokumentowane ograniczenia zakresu. Okres, waluta i zakres muszą się zgadzać przed interpretacją różnicy.
Co wykazały testy przykładu?
Osiem równoległych prób rezerwacji dopuściło dokładnie dwie kwoty po 40 przy limicie 100. Dalsze testy izolowane sprawdzały powtórne rozliczenia, nierozstrzygnięte próby, sprzeczne limity, błędne kwoty i granice miesięcy UTC. Nie testowano płatnych wywołań API ani prawdziwej faktury.