LW IT Solutions
« Blog Overview /Cloud & AI/Tutorials / Tutorial: Pomiar zużycia tokenów na żądanie i...
This post in other languages:

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

Tutorial: Pomiar zużycia tokenów na żądanie i egzekwowanie budżetu miesięcznego
Spis treści
  1. Gdzie prawdziwa liczba już stoi
  2. Liczenie przed wysłaniem
  3. Jeden wiersz na żądanie
  4. Zamiana tokenów na pieniądze
  5. Blokada, która naprawdę coś zatrzymuje
  6. Czego blokada nie widzi
  7. Źródła

Faktura za API przychodzi raz w miesiącu i podaje sumę. Między tą sumą a kodem, który ją wytworzył, zwykle nie stoi nic – żadnej notatki, która funkcja ile zużyła, i żadnej możliwości odróżnienia drogiego tygodnia od drogiego nawyku.

Lukę da się łatwo domknąć, bo informacja tkwi już w każdej odpowiedzi. Poniższa praca to dwadzieścia linii zapisu, jedna tabela cen i jedno sprawdzenie działające przed żądaniem, a nie po nim.

Paragon za pojedyncze żądanie z czterema osobno wycenionymi klasami tokenów, obok pasek budżetu miesięcznego z progiem ostrzegawczym i twardym zatrzymaniem
To samo żądanie, cztery ceny. 96 000 tokenów odczytanych z pamięci podręcznej kosztuje mniej niż 12 400 wysłanych na świeżo – dlatego jedna zbiorcza liczba tokenów niczego nie wyjaśnia.

Gdzie prawdziwa liczba już stoi

Każda odpowiedź uzupełnienia czatu niesie blok usage. Nie jest to szacunek ani zaokrąglenie, lecz to, co zapisał licznik.

{
  "usage": {
    "input_tokens": 12400,
    "cache_creation_input_tokens": 8200,
    "cache_read_input_tokens": 96000,
    "output_tokens": 1850
  }
}

Te cztery pola to cztery różne ceny, a podział waży więcej niż suma. Świeżo wysłane wejście to stawka podstawowa. Tokeny zapisane do pamięci podręcznej promptu kosztują jednorazową dopłatę. Tokeny z niej odczytane kosztują ułamek stawki podstawowej. Wyjście jest klasą najdroższą ze wszystkich, zwykle wielokrotnością ceny wejścia.

Inni dostawcy nazywają te same rzeczy inaczej – prompt_tokens, completion_tokens i zagnieżdżone cached_tokens to zwykła odmiana – lecz budowa jest ta sama, a wniosek również: pojedynczej liczby z etykietą „tokeny” nie da się przeliczyć na pieniądze.

Do strumieniowania należy jedna uwaga. Blok usage nie przychodzi z pierwszym kawałkiem, lecz na końcu, w zdarzeniu zamykającym strumień. Kod czytający tekst i odrzucający resztę traci więc dokładnie tę część, która coś kosztuje.

Liczenie przed wysłaniem

Czasem liczba potrzebna jest, zanim żądanie w ogóle powstanie – aby rozstrzygnąć, czy dokument jeszcze się mieści albo czy rozmowę trzeba skrócić. Służy do tego punkt zliczający, bezpłatny i zwracający to, co zobaczyłby model.

curl https://api.anthropic.com/v1/messages/count_tokens \
  -H "x-api-key: $API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "messages": [{"role": "user", "content": "..." }]
  }'

Lokalny tokenizer odpowiada na to samo pytanie bez wywołania sieciowego i wystarcza do sprawdzenia długości, ale nie jest licznikiem. Instrukcje systemowe, definicje narzędzi i bloki obrazów wnoszą tokeny, których tokenizer tekstu nigdy nie widzi, a różnica przy żądaniu z dołączonymi narzędziami idzie w tysiące. Dla budżetu liczy się wyłącznie blok usage.

Jeden wiersz na żądanie

Sam zapis jest niewidowiskowy, a jego wartość leży w całości w polach opisujących kontekst, a nie żądanie. Model i liczby tokenów same z siebie nie odpowiadają na nic; oznaczenie, która funkcja wywołała żądanie, odpowiada niemal na wszystko.

import json, sqlite3, time, uuid

DB = sqlite3.connect("verbrauch.db")
DB.execute("""CREATE TABLE IF NOT EXISTS aufrufe (
    id TEXT PRIMARY KEY, zeit INTEGER, modell TEXT, zweck TEXT,
    ein INTEGER, cache_neu INTEGER, cache_gelesen INTEGER, aus INTEGER,
    kosten REAL, versuch INTEGER)""")

def buchen(antwort, modell, zweck, versuch=1):
    u = antwort["usage"]
    zeile = dict(
        id=str(uuid.uuid4()), zeit=int(time.time()), modell=modell, zweck=zweck,
        ein=u["input_tokens"],
        cache_neu=u.get("cache_creation_input_tokens", 0),
        cache_gelesen=u.get("cache_read_input_tokens", 0),
        aus=u["output_tokens"], versuch=versuch)
    zeile["kosten"] = kosten(modell, zeile)
    DB.execute("INSERT INTO aufrufe VALUES (:id,:zeit,:modell,:zweck,:ein,"
               ":cache_neu,:cache_gelesen,:aus,:kosten,:versuch)", zeile)
    DB.commit()
    return zeile["kosten"]

Kolumna versuch zwraca się w sposób nieoczekiwany. Ponowna próba po przekroczeniu czasu wysyła całe wejście jeszcze raz i zostaje naliczona jeszcze raz, podczas gdy aplikacja widzi jedno logiczne wywołanie. Bez tej kolumny powtórki chowają się w średniej, a zły popołudniowy przebieg wygląda jak droga funkcja.

Zamiana tokenów na pieniądze

Ceny należą do jednego miejsca, według modelu i klasy tokenów, podane za milion tokenów – bo tak są publikowane.

# Przykładowe stawki za milion tokenów - prawdziwe stoją
# w cenniku dostawcy i zmieniają się w czasie.
PREISE = {
    "claude-sonnet-5": {"ein": 3.00, "cache_neu": 3.75, "cache_gelesen": 0.30, "aus": 15.00},
}

def kosten(modell, z):
    p = PREISE[modell]
    return round(sum(z[k] * p[k] for k in ("ein", "cache_neu", "cache_gelesen", "aus")) / 1e6, 6)

Zastosowane do powyższego żądania, cztery klasy dają 0,0372, 0,0308, 0,0288 oraz 0,0278 – razem 0,1245 na wywołanie. Pouczające jest zestawienie trzeciej wartości z pierwszą: 96 000 tokenów odczytanych z pamięci podręcznej kosztuje mniej niż 12 400 wysłanych na świeżo. Działająca pamięć podręczna nie tylko obniża więc rachunek, lecz przesuwa też to, która część żądania go wyznacza.

Gdy w tabeli leży kilka dni wierszy, właściwe pytanie daje się rozstrzygnąć jedną instrukcją.

SELECT zweck,
       COUNT(*)                          AS aufrufe,
       ROUND(SUM(kosten), 2)             AS gesamt,
       ROUND(AVG(kosten), 4)             AS je_aufruf,
       ROUND(100.0 * SUM(cache_gelesen) /
             NULLIF(SUM(ein + cache_neu + cache_gelesen), 0), 1) AS cache_anteil
FROM aufrufe
WHERE zeit >= strftime('%s', 'now', 'start of month')
GROUP BY zweck ORDER BY gesamt DESC;

Blokada, która naprawdę coś zatrzymuje

Budżet, który jedynie ostrzega, jest raportem. Zatrzymanie wymaga, by sprawdzenie stało przed wywołaniem – i by liczba była bieżąca, a nie z ostatniej nocy.

class BudgetErschoepft(Exception):
    pass

MONATSBUDGET = 400.0
WARNSCHWELLE = 0.80

def monatssumme():
    (s,) = DB.execute(
        "SELECT COALESCE(SUM(kosten), 0) FROM aufrufe "
        "WHERE zeit >= strftime('%s', 'now', 'start of month')").fetchone()
    return s

def pruefen(geschaetzte_kosten):
    verbraucht = monatssumme()
    if verbraucht + geschaetzte_kosten > MONATSBUDGET:
        raise BudgetErschoepft(f"{verbraucht:.2f} von {MONATSBUDGET:.2f} verbraucht")
    if verbraucht > MONATSBUDGET * WARNSCHWELLE:
        logging.warning("Budget zu %.0f %% verbraucht", 100 * verbraucht / MONATSBUDGET)

Trzy rozstrzygnięcia czynią z tego coś więcej niż formalność. Przekazywany szacunek powinien być pesymistyczny – najwyższa możliwa liczba tokenów wyjścia po cenie wyjścia – bo wywołanie jest albo dozwolone, albo nie, a zgoda na podstawie optymistycznej to droga do przekroczenia budżetu o dokładnie jedno żądanie.

Drugie dotyczy tego, co dzieje się po zgłoszeniu wyjątku. Przebieg wsadowy powinien się zatrzymać i powiedzieć, gdzie stanął. Funkcja w dialogu powinna ustąpić, a nie polec: mniejszy model, krótszy kontekst albo kolejka wznawiana w następnym miesiącu. Jedno i drugie jest lepsze niż ślad błędu docierający do czytelnika.

Trzecie: wspólny budżet potrzebuje wspólnego licznika. Kilka procesów z własnymi sumami każdy z osobna zmieści się pod granicą i wspólnie ją przekroczy. Jedna tabela z transakcją obejmującą odczyt i zapis albo licznik we wspólnej pamięci to cała naprawa.

Czego blokada nie widzi

Trzy rodzaje wydatków nigdy nie przechodzą przez sprawdzenie, a każdy ma własną drogę, by mimo to zostać ujęty.

Przetwarzanie wsadowe naliczane jest z rabatem i biegnie często w tle, więc blok usage przychodzi wiele godzin później wraz z wynikami. Jego koszt należy do tej samej tabeli, wpisany przy odbiorze wyników, ale z czasem zgłoszenia, a nie odbioru – inaczej na granicy miesięcy wydatek trafi do niewłaściwego miesiąca.

Drugim przypadkiem są żądania nieudane. Wywołanie wpadające w przekroczenie czasu po tym, jak model wytworzył już większość odpowiedzi, i tak zostaje naliczone, a bloku usage do odczytu nie dostarcza. Dostarcza natomiast identyfikator żądania w nagłówkach odpowiedzi, a raport zużycia u dostawcy go prowadzi. Comiesięczne uzgodnienie własnej tabeli z tym raportem kosztuje dziesięć minut i znajduje dokładnie tę klasę rozbieżności.

Trzecim przypadkiem jest wszystko, co nie jest uzupełnieniem czatu: osadzenia, transkrypcje, generowanie obrazów, narzędzie wyszukiwania wywołane po stronie serwera. Każde ma własny licznik i własną cenę, a żadne z nich nie pojawia się w zapisywanym tutaj bloku usage. Uzgodnienie wyłapuje i te – i to praktyczny powód, by je w ogóle prowadzić.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Napisanie komentarza

Adres e-mail nie jest publikowany. Pola obowiązkowe oznaczono gwiazdką.

ALL ARTICLES & CATEGORIES

CCTV

Śledź tę kategorię przez RSS

Cloud & AI

Śledź tę kategorię przez RSS

Data Privacy

Wszystkie artykuły w tej kategorii (12) Śledź tę kategorię przez RSS

Digital Analytics

Wszystkie artykuły w tej kategorii (47) Śledź tę kategorię przez RSS

Digital Marketing

Wszystkie artykuły w tej kategorii (25) Śledź tę kategorię przez RSS

IT & Networks

Wszystkie artykuły w tej kategorii (16) Śledź tę kategorię przez RSS

Raspberry Pi

Śledź tę kategorię przez RSS

Smart Home

Wszystkie artykuły w tej kategorii (14) Śledź tę kategorię przez RSS

Tworzenie stron internetowych

Śledź tę kategorię przez RSS

Wtyczki i triki WordPress

Śledź tę kategorię przez RSS