Historien er alltid den samme. Et lite skript kaller et betalt API. Noe feiler. Skriptet prøver igjen, og igjen, og igjen, fordi du (rimelig nok) ba det om å prøve på nytt ved feil. Når du ser regningen, har løkka kjørt hele natten.
Det er ikke et dumt-menneske-problem. Det er et manglende-sikringsskap-problem. Du har strøm i huset og ingen sikringer, og det går fint til det ikke gjør det.
Fire lag, ikke ett #
Et varsel er ikke et tak. Et tak hos leverandøren er ikke det samme som en stoppeklosse i koden din. Du trenger alle fire lagene, og de gjør ulike jobber:
- Måling: du vet hva hvert kall koster.
- Varsling: du får beskjed før det blir dyrt.
- Kvoter og tak hos leverandøren: et ytre gjerde.
- Avbryter i egen kode: det som stopper løkka på stedet.
1. Måling: logg kostnaden per kall #
API-svar har normalt et felt som oppgir hvor mange tokens (eller andre enheter) kallet brukte. Feltnavnene varierer fra leverandør til leverandør, så sjekk dokumentasjonen din. Logg dem sammen med tidspunkt og hvilken jobb som kalte. Regn om til penger med priser du henter fra leverandørens prisside, ikke fra hukommelsen din og ikke fra denne artikkelen.
Uten måling vet du ikke om dagens forbruk er normalt. Med måling får du et sammenligningsgrunnlag, og avvik blir synlige.
2. Varsling: sett terskler, og forstå hva de ikke gjør #
Sett et varsel ved for eksempel halvparten og 80 prosent av det du er villig til å bruke på en dag eller en måned. Det er nyttig, men ikke mer enn det.
OpenAI sier det rett ut: varsler om forbruk sender et varsel mens API-trafikken fortsetter. De begrenser ikke noe. Hos AWS er situasjonen lik: AWS Budgets er først og fremst varsling, og dokumentasjonen sier at budsjettdata oppdateres opptil tre ganger i døgnet, typisk 8–12 timer mellom hver oppdatering, og at du kan få kostnader over terskelen før varselet kommer. Dokumentasjonen nevner at du kan koble en handling til et varsel, som å automatisk sette en IAM-policy som hindrer nye ressurser, men det er noe du må sette opp selv. Det er ikke et innebygd tak.
Konklusjon: varsler er for mennesker som våkner. De stopper ikke en løkke klokken tre om natten.
3. Kvoter og tak hos leverandøren #
Noen leverandører har faktiske sperrer, og da bør du bruke dem. Men les detaljene:
- OpenAI har harde grenser på organisasjons- og prosjektnivå. Når de nås, får trafikken
429med feilkodeneorganization_spend_limit_exceededellerproject_spend_limit_exceeded. Men dokumentasjonen understreker at håndhevingen ikke er øyeblikkelig, så du kan ende litt over grensen mens status spres. - Anthropic har månedlige forbruksgrenser. Når taket for ditt nivå nås, pauses bruken til første dag i neste måned, og kallene får
429medenforced_spend_limit_reached, utenretry-after-header. Dokumentasjonen sier at nye forsøk, også SDK-ets automatiske, vil feile helt til tilgangen er tilbake. Setter du en egen, lavere grense, får du400medinvalid_request_errori stedet. Du kan sette grenser per workspace, men ikke på standard-workspacet. - Andre leverandører har kanskje bare varsler, kanskje en grense som oppdateres med forsinkelse, kanskje ingenting. Ikke anta. Les det selv, og skriv ned: er dette varsel eller sperre, og hvor sen er den?
Praktisk råd: bruk egen API-nøkkel (eller eget prosjekt) per applikasjon. Da kan du sette grenser per jobb, og en løpsk nattejobb tar ikke ned alt annet.
Legg også merke til hva Anthropic-eksemplet lærer oss. En klient som prøver på nytt ved enhver feil, vil hamre på en vegg i flere uker hvis feilen er «du har brukt opp månedsgrensen». Retry er for midlertidige feil. «Du er tom for penger» er ikke midlertidig.
4. Avbryteren i egen kode #
Dette er laget du har full kontroll over, og som virker uansett hva leverandøren gjør. Prinsippet er å sjekke før hvert kall, og stoppe hardt:
import datetime as dt
import sqlite3
import sys
# Priser per million tokens: hent fra leverandørens prisside. Null er ikke lov.
PRIS_INN_PER_MTOK = 0.0
PRIS_UT_PER_MTOK = 0.0
MAKS_PER_KJORING = 5.0 # i fakturavalutaen
MAKS_PER_DAG = 20.0
VARSEL_PER_DAG = 10.0
MAKS_KALL_PER_KJORING = 200
MAKS_FEIL_PAA_RAD = 3
class BudsjettOverskredet(Exception):
pass
class Vakt:
def __init__(self, db_path="kost.db"):
if PRIS_INN_PER_MTOK <= 0 or PRIS_UT_PER_MTOK <= 0:
raise RuntimeError("Fyll inn priser fra prissiden først.")
self.db = sqlite3.connect(db_path)
self.db.execute(
"CREATE TABLE IF NOT EXISTS kost (dag TEXT, inn INT, ut INT, kost REAL)")
self.kjoring, self.kall, self.feil_paa_rad = 0.0, 0, 0
self.varslet = False
def dag_total(self):
dag = dt.date.today().isoformat()
rad = self.db.execute(
"SELECT COALESCE(SUM(kost), 0) FROM kost WHERE dag = ?", (dag,)).fetchone()
return rad[0]
def for_kall(self):
if self.kall >= MAKS_KALL_PER_KJORING:
raise BudsjettOverskredet("for mange kall i denne kjøringen")
if self.kjoring >= MAKS_PER_KJORING:
raise BudsjettOverskredet("kjøringens tak er nådd")
if self.dag_total() >= MAKS_PER_DAG:
raise BudsjettOverskredet("dagens tak er nådd")
if self.feil_paa_rad >= MAKS_FEIL_PAA_RAD:
raise BudsjettOverskredet("for mange feil på rad")
self.kall += 1
def etter_kall(self, tokens_inn, tokens_ut):
kost = (tokens_inn / 1e6 * PRIS_INN_PER_MTOK
+ tokens_ut / 1e6 * PRIS_UT_PER_MTOK)
self.kjoring += kost
self.feil_paa_rad = 0
self.db.execute("INSERT INTO kost VALUES (?, ?, ?, ?)",
(dt.date.today().isoformat(), tokens_inn, tokens_ut, kost))
self.db.commit()
if not self.varslet and self.dag_total() >= VARSEL_PER_DAG:
self.varslet = True
print("VARSEL: dagens forbruk har passert terskelen", file=sys.stderr) # bytt med e-post
def feil(self):
self.feil_paa_rad += 1 Og brukt rundt selve kallet:
vakt = Vakt()
for oppgave in oppgaver:
vakt.for_kall() # kaster hvis noe tak er nådd
try:
svar = kall_api(oppgave) # din egen funksjon
except Exception:
vakt.feil()
raise # ingen blind retry
inn, ut = hent_forbruk(svar) # din funksjon: les svarets usage-felt
vakt.etter_kall(inn, ut) Koden er et skall som illustrerer prinsippet, ikke et ferdig bibliotek, og den er ikke testet mot noen bestemt leverandør. Tre ting er viktige å få med seg:
- Terskelen for å stoppe er lavere enn det du kan tåle. Siden håndhevingen hos leverandøren kan være forsinket, og siden ett kall kan koste mer enn gjennomsnittet, bør dagens tak ligge godt under den summen som faktisk gjør vondt.
- Dagssummen lagres utenfor prosessen. Ellers nullstilles «dagens forbruk» hver gang skriptet starter på nytt, og en omstartsløkke omgår taket. Derfor databasen.
- Retry har et eget tak. Prøv på nytt med økende ventetid for midlertidige feil, og gi opp raskt for alt annet. Tre feil på rad er en grunn til å stoppe og si fra, ikke til å prøve en fjerde gang.
Så hva er et «tak»? #
Slik kan du sortere det du har:
- Varsel: forteller deg at noe skjer. Stopper ingenting.
- Leverandørens tak: stopper trafikken, men kanskje med forsinkelse, og du bør vite hva feilkoden betyr slik at koden ikke prøver i evigheter.
- Taket i din kode: stopper umiddelbart, men bare det du selv har skrevet det inn i.
Ingen av dem er et fullverdig sikkerhetsnett alene. Sammen er de det som skiller en dyr nattesøvn fra en kjedelig morgen. Kjedelig er målet.
