Dostępny na nowe rolePiotr Czerwiński

Blog · 27 sierpnia 2026 · 9 min czytania

Każdy endpoint, który wywołuje model, to otwarty portfel

AI · bezpieczeństwo · optymalizacja kosztów · LLM

TL;DR: Każda ścieżka, która wywołuje płatny model, zamienia pętlę, bota albo jednego ciekawskiego użytkownika w rachunek. Moja zasada jest dziś stała: każdy endpoint wywołujący model dostaje limit na użytkownika, globalny dzienny limit kwotowy i kill switch (wyłącznik awaryjny), który działa bez deployu, a każdy publiczny formularz przed takim endpointem dostaje weryfikację antybotową. Liczniki działają w trybie fail closed, więc zepsuty licznik nigdy nie oznacza nieograniczonego budżetu. Każde wywołanie modelu zapisuje trwały wiersz z kosztem w rejestrze wydatków wewnątrz produktu. Środowiska są rozdzielone u dostawcy, każde z własnym projektem i twardym limitem, więc liczby z produkcji pozostają czyste. Same limity wydatków u dostawcy nie wystarczą; niektóre nie są nawet twarde.

Problem: zalogowany to nie to samo co objęty budżetem

W produkcie do mierzenia widoczności w AI, który prowadzę, kosztowna praca (cykliczne zadawanie pytań silnikom AI) działała w procesie w tle, a ten proces od początku miał twardy dzienny limit w dolarach. Panel klienta był osobną usługą. Kilka jego ścieżek też wywoływało model: generowanie sugestii podczas onboardingu, pisanie briefów treści dla klienta. Te ścieżki sprawdzały, czy użytkownik jest zalogowany, i nic więcej.

Przy otwartej rejestracji „zalogowany” znaczy „każdy, kto wypełnił formularz”. Skrypt, który zakłada konto i wywołuje jedną z tych ścieżek w pętli, wydaje moje pieniądze w takim tempie, na jakie pozwala dostawca. W sensie bezpieczeństwa nic nie jest zepsute. Uwierzytelnianie działa, dane są poprawnie ograniczone do właściciela, nikt nie czyta niczego, czego nie powinien. Ścieżka po prostu nie ma pojęcia, ile wolno jej kosztować.

Branżowa nazwa tego zjawiska to denial of wallet: atak albo wypadek, którego szkodę mierzy się rachunkiem za chmurę lub API, a nie przestojem. OWASP Top 10 dla aplikacji LLM zalicza go do kategorii unbounded consumption. W produkcie prowadzonym w pojedynkę to najbardziej prawdopodobny poważny incydent, bo nie wymaga żadnych umiejętności.

Jakich zabezpieczeń potrzebuje endpoint, który wywołuje LLM?

Wszędzie skończyło się na tych samych warstwach, ułożonych od najtańszego sprawdzenia, żeby zablokowane zapytanie kosztowało jeden szybki odczyt:

  1. Globalny kill switch. Jedna flaga w bazie danych, którą respektuje każda ścieżka generująca wydatki, w każdej usłudze. Jej przełączenie nie wymaga deployu, a krótki cache zdejmuje odczyt z gorącej ścieżki i wciąż dociera do każdej instancji w ciągu kilku sekund. Stoi ponad wszystkim innym: zatrzymuje wydatki nawet wtedy, gdy dzienny limit jest wyłączony.
  2. Limit na użytkownika. Godzinowa pula na konto i ścieżkę. Chroni portfel, a także pozostałych klientów, o czym niżej.
  3. Globalny dzienny limit. Sufit w dolarach dla wszystkich użytkowników i wszystkich ścieżek. Ogranicza zasięg szkód, gdy działa wiele kont naraz, czego limity na użytkownika nie potrafią.
  4. Weryfikacja antybotowa na publicznych formularzach. Rejestracja, logowanie, reset hasła i każdy anonimowy formularz, który prowadzi do wywołania modelu, dostają challenge dla botów i limit zapytań na IP.
// illustrative shape - the guard in front of a model call
const verdict = await checkQuota({ user, route });
// order: kill switch -> per-user window -> global daily spend
if (!verdict.ok) return tooMany(verdict.retryAfterSec);

const result = await model.run(input);
await recordSpend({ route, user, usage: result.usage }); // durable row

Anonimowi użytkownicy mają własny zestaw limitów, pod własnymi nazwami w konfiguracji, oddzielony od limitów dla zalogowanych, więc zaostrzenie publicznej witryny sklepowej nigdy nie poluzuje onboardingu i odwrotnie. Identyczne powtórzone zapytania obsługuje cache odpowiedzi i w ogóle nie docierają do strażnika, więc pula schodzi tylko na naprawdę nowe dane wejściowe. Właściciel konta omija wszystkie limity, co jest spójne z tym, jak w produkcie działają już limity planów.

Na płatnej ścieżce najskuteczniejszym zabezpieczeniem jest kolejność. Kosztowne generowanie jednorazowego raportu rusza dopiero po potwierdzeniu płatności. Darmowa część to szkic; wywołanie modelu uruchamia płacący klient, więc bot wypełniający formularz nic nie kosztuje.

Opcje i dlaczego żadna nie wystarcza w pojedynkę

Tylko limity wydatków u dostawcy. To ostatnia linia obrony i trzeba je ustawić, ale są zgrubne (na projekt albo konto, a nie na użytkownika), reagują powoli i nie zawsze są twarde. U jednego dostawcy, z którego korzystałem, limit wydatków pozwolił kontu wyjść ponad przedpłacone saldo. Konto przedpłacone z twardym saldem to prawdziwy sufit; limit, który dopuszcza przekroczenie, to w gruncie rzeczy alert, tylko bardziej skomplikowany.

Tylko limity na użytkownika. Powstrzymują jedno konto przed zasypywaniem endpointu zapytaniami. Nic nie dają przeciw stu świeżym kontom, które przy otwartej rejestracji kosztują atakującego kilka minut.

Tylko globalny limit. Ten wariant ma słaby punkt, który znalazłem we własnym audycie. Dzienny limit wspólny dla wszystkich użytkowników, bez limitu na użytkownika na jednej ze ścieżek przed nim, oznacza, że jedno konto próbne może wypalić wspólny budżet i do końca dnia zablokować zaplanowaną pracę każdego płacącego klienta. Limit zamienia problem kosztowy w problem dostępności. Limity na użytkownika istnieją po to, żeby wspólnego limitu nie dało się użyć jako dźwigni do ataku denial of service.

Tylko weryfikacja antybotowa. Podnosi koszt automatyzacji i ma własną cichą awarię: u mnie brak sekretu do challenge sprawiał, że weryfikacja zwracała sukces dla wszystkiego, bez błędu i bez logu. Captcha była wyłączona, a strona wyglądała tak samo. Po każdej rotacji sekretów sprawdzam teraz, czy sekret nadal jest ustawiony w usłudze, która go weryfikuje.

Dlatego stosuję wszystkie cztery, a to połączenie jest tanie: kilka odczytów po indeksie na zapytanie i jedna tabela na przełącznik.

Fail closed, z wyjątkiem miejsc, w których nie wolno

Strażnik odczytuje liczniki z bazy danych. Jeśli ten odczyt się nie powiedzie, strażnik odmawia wydatku i zwraca retry-after. Logika jest prosta: jeśli nie umiem udowodnić, że jesteśmy poniżej limitu, nie wydajemy. Alternatywa, w której czkawka bazy danych po cichu zdejmuje naraz wszystkie hamulce, to dokładnie ten moment, którego nie chcesz odkryć.

Dwa świadome wyjątki chronią przed awariami zadanymi samemu sobie. Brak tabeli kill switcha oznacza „nie zatrzymano”, bo świeża baza po prostu nigdy nie była przełączana. A dzienny limit jest domyślnie wyłączony: źle ustawiony limit nigdy nie może po cichu zabić uprawnionej pracy. Gdy limit jest już skonfigurowany, odczyty działają w trybie fail closed. Fail closed chroni limity, które ktoś wybrał; brakująca konfiguracja dostaje własną bezpieczną wartość domyślną.

Ograniczeniem stałego limitu w dolarach jest wzrost. Statyczny sufit dobrany do dzisiejszej skali odciąłby uprawnioną pracę w dobry dzień z dużą liczbą rejestracji. Plan, który mam na to spisany, to dynamiczny limit liczony jako liczba aktywnych subskrypcji razy budżet na klienta, żeby sufit rósł razem z przychodem, a nie z ruchem.

Skąd wiadomo, ile naprawdę kosztują funkcje AI?

Potrzebujesz rejestru wydatków w produkcie i środowisk, które go nie zaśmiecają.

Rejestr. Każde wywołanie modelu zapisuje trwały wiersz: ścieżka, konto, surowe liczby tokenów wejściowych i wyjściowych oraz wyliczony koszt. Surowe tokeny są ważne, bo dostawcy zmieniają ceny, a historię chcesz móc przeliczyć. Okno limitu zapytań to inna tabela z innym zadaniem: liczy wywołania, jest czyszczona po kilku dniach i nie służy do księgowania. Mieszanie jednego z drugim sprawiło, że mój pierwszy panel kosztów sumował tylko pracę silnika w tle, a dwie płatne ścieżki z modelem w panelu klienta były niewidoczne zarówno w panelu kosztów, jak i w limicie. Od tamtej pory obowiązuje zasada: nowy endpoint wywołujący model nie jest skończony, dopóki jego koszt nie trafia do rejestru i nie wlicza się do limitu.

Rozdzielenie u dostawcy. Środowisko jest rozdzielone tam, gdzie żyje budżet; nazwa klucza to tylko etykieta. Gdy dostawca obsługuje projekty albo tryby, produkcja i development dostają osobne projekty, a projekt deweloperski ma własny, mały, twardy limit; odpowiedź na pytanie „ile kosztuje development” odczytuję wtedy z panelu dostawcy dla każdego projektu. Gdy dostawca nie ma trybu testowego, środowiska nieprodukcyjne po prostu go nie wywołują: lokalnie kody jednorazowe wypisują się w terminalu zamiast iść przez dostawcę e-maili, a skany nie działają bez procesu w tle. Klucz produkcyjny nigdy nie trafia do lokalnego pliku, zarówno ze względu na zasięg ewentualnych szkód, jak i dlatego, że wywołania z developmentu zamazywałyby koszty produkcji.

Gdy działa jedno i drugie, rejestr na produkcji mierzy produkcję i nic więcej, a dopiero to czyni go przydatnym przy decyzjach cenowych.

Jedno ustawienie modelu na każde zadanie

Ostatnia dźwignia to wybór modelu dla każdego miejsca i zmiana tego wyboru bez deployu. Panel administracyjny dostępny tylko dla właściciela (za drugim składnikiem uwierzytelniania) udostępnia w czasie działania kill switch, dzienny limit i model dla każdego zadania: jedno ustawienie dla płatnego produktu, jedno dla darmowych albo przedzakupowych pomocników, jedno dla klasyfikacji. Dzięki rozdzieleniu podniesienie jakości dla płacących klientów nigdy po cichu nie podnosi kosztu darmowego narzędzia, które może uruchomić każdy w internecie.

Jeden szczegół ma większe znaczenie, niż się wydaje: każde miejsce wywołania musi ustalać model przez tę samą funkcję. Miałem kiedyś dwa przepływy korzystające z tego samego rodzaju generowania, z których jeden pomijał nadpisanie z panelu, więc podniesienie modelu w panelu poprawiało tylko jeden z nich. Jedna funkcja rozstrzygająca z jasną kolejnością (nadpisanie z panelu admina, potem zmienna środowiskowa, potem wartość domyślna) usunęła całą tę klasę rozjazdów. To, czy tańszy model wystarczy do danego zadania, rozstrzyga eval; jak je prowadzę, opisałem w tekście o evalach przed merge.

Co powiedziałbym komuś, kto jutro wdraża funkcję z LLM

  • Traktuj „wywołuje model” jako właściwość bezpieczeństwa ścieżki. Limit na użytkownika, globalny dzienny limit, kill switch, zanim zobaczy ją pierwszy użytkownik.
  • Dodaj challenge i limit na IP do każdego publicznego formularza, który prowadzi do wydatków, i po każdej zmianie sekretu sprawdź, czy challenge naprawdę działa.
  • Ustaw liczniki w trybie fail closed, ale limit kwotowy domyślnie trzymaj wyłączony, dopóki ktoś go świadomie nie ustawi.
  • Kill switch ma być globalny i działać bez deployu. Jeśli wymaga deployu, w dniu, w którym go potrzebujesz, okaże się za wolny.
  • Pobieraj płatność przed kosztownym wywołaniem wszędzie, gdzie produkt na to pozwala.
  • Zapisuj każde wywołanie w rejestrze z surowymi tokenami i trzymaj ten rejestr oddzielnie od okna limitu zapytań.
  • Rozdziel środowiska u dostawcy, z małym twardym limitem dla developmentu, a tam, gdzie limit nie jest naprawdę twardy, wybieraj przedpłacone saldo.
  • Daj każdemu zadaniu własne ustawienie modelu, ustalane w jednym miejscu.

Zabezpieczenia ograniczają, ile może kosztować wywołanie, ale nie sprawiają, że jego wynik jest wiarygodny. Ta druga połowa problemu to schematy i walidacja, o których napisałem w tekście o tym, dlaczego wolę tool use od parsowania.

Pytania, na które odpowiada ten wpis

Jak powstrzymać użytkowników przed nabiciem rachunku za API OpenAI albo innego LLM?
Postaw trzy zabezpieczenia przed każdym endpointem, który wywołuje model: limit zapytań na użytkownika, globalny dzienny limit kwotowy i kill switch, który działa bez deployu. Dodaj weryfikację antybotową i limity na IP na publicznych formularzach, a liczniki ustaw w trybie fail closed, żeby zepsuty licznik nigdy nie oznaczał nieograniczonego budżetu.
Czym jest denial of wallet w aplikacjach z LLM?
Denial of wallet to atak albo wypadek, którego szkodę mierzy się rachunkiem za API lub chmurę, a nie przestojem, na przykład skrypt wywołujący w pętli endpoint oparty na modelu. OWASP Top 10 dla aplikacji LLM opisuje to zagrożenie jako unbounded consumption, czyli nieograniczone zużycie zasobów.
Czy limity wydatków u dostawcy wystarczą do kontroli kosztów LLM?
Nie. Limity u dostawcy są zgrubne, działają na poziomie projektu, a nie użytkownika, i nie zawsze są twarde; niektóre pozwalają wyjść ponad przedpłacone saldo. Traktuj je jako ostatnią linię obrony, z osobnymi projektami u dostawcy dla każdego środowiska, a własne limity na użytkownika, dzienny limit i rejestr wydatków trzymaj w produkcie.