CodeGym /Kursy /ChatGPT Apps /Kontrola kosztów i instrumentacja cost

Kontrola kosztów i instrumentacja cost

ChatGPT Apps
Poziom 19 , Lekcja 0
Dostępny

1. Dlaczego „działa” ≠ „się zwraca”

Aplikacje LLM mają ważną cechę: oprócz stałych kosztów hostingu często pojawiają się zmienne koszty wykonania niektórych żądań, związane z wywołaniami modeli.

Warto rozróżniać dwa światy:

  • gdy model działa po stronie ChatGPT (użytkownik rozmawia z Twoją aplikacją w ChatGPT, a ona wywołuje mcp-tools) — za tokeny płaci użytkownik swoją subskrypcją ChatGPT;
  • gdy Twój backend/serwer MCP sam wywołuje OpenAI API lub inne usługi LLM — za te tokeny płacisz już Ty.

Właśnie w drugim przypadku pojawiają się klasyczne zmienne koszty LLM, które zależą od liczby i „ciężaru” (tokens_in/tokens_out) żądań.

Klasyczny scenariusz:

  1. Z radością wypuszczasz GiftGenius na produkcję, wszystko śmiga, użytkownicy są szczęśliwi.
  2. Po miesiącu przychodzi rachunek za OpenAI + chmurę + prowizje Stripe i nagle okazuje się, że „udany wzrost” tak naprawdę znaczy „płacimy za każdy prezent więcej, niż zarabiamy na sprzedaży”.

Podejście FinOps mówi: koszt to taka sama metryka jak latencja czy error‑rate. Trzeba ją logować, agregować i na jej podstawie podejmować decyzje, a nie „zgadywać w Excelu”.

Cel tego wykładu — sprawić, byś mógł odpowiadać na pytania typu:

  • „Ile kosztował ten konkretny dobór prezentu dla użytkownika user42?”
  • „Ile pieniędzy w tym tygodniu spaliło narzędzie suggest_gifts i ile przy tym przyniosło zamówień?”

I żeby odpowiedzi brały się nie z powietrza, lecz z logów i metryk.

2. Struktura kosztów ChatGPT App

Zacznijmy od mapy wydatków. Bez niej wszystko pozostałe to chaotyczne zbieranie liczb.

Koszty LLM (zmienne)

To wszystko, co jest związane z wywołaniami modeli z Twojego backendu:

  • Wywołania modeli OpenAI z serwera MCP lub agentów: GPT-5.1 / GPT-5-mini / embeddings / rerank / vision / TTS/STT itp.
  • Dodatkowe modele: reranking do wyszukiwania, embeddingi do rekomendacji, generowanie obrazów.

Ważny niuans: gdy budujesz interfejs przez Apps SDK i używasz tylko wbudowanego modelu ChatGPT, nie płacisz za tokeny — płaci użytkownik (swoją subskrypcją ChatGPT). Ale gdy tylko Twój serwer MCP sam zaczyna wywoływać OpenAI API (Agents, Responses API, embeddings itp.), tokeny idą już na Twoje konto.

Idea bazowa: koszt takich wywołań jest proporcjonalny do tokens_in i tokens_out pomnożonych przez cenę za token.

Wywołanie narzędzia MCP samo w sobie jest bezpłatne dla dewelopera z punktu widzenia tokenów; wydatki pojawiają się tylko tam, gdzie w jego handlerze decydujesz się wywołać OpenAI API lub inny LLM.

Infrastruktura

To całe „żelazo” i usługi wokół:

  • Serwery MCP: Vercel / AWS / GCP / bare metal.
  • Agenci (jeśli działają jako osobne serwisy).
  • Bazy danych: Postgres/MySQL, bazy wektorowe, S3/magazyny obiektowe.
  • Cache: Redis/KeyDB.
  • Kolejki i workerzy: na przykład do generowania w tle, przeliczeń feedów itp.

Te koszty częściej są stałe w skali miesiąca (lub schodkowo‑stałe), dlatego zwykle liczy się je na podstawie zagregowanych danych z wydatków na usługi chmurowe, a nie dla każdego żądania.

Płatności i usługi zewnętrzne

U GiftGenius jest ACP/Stripe, a więc pojawiają się:

  • Prowizje za każdą udaną płatność (Stripe rzędu kilku procent + część stała).
  • Straty na fraud i chargebackach.
  • Koszt zewnętrznych API: e‑mail / SMS / powiadomienia push, dodatkowa analityka itp.

Na starcie to grosze, ale przy skali zaczyna być odczuwalne, więc przynajmniej na poziomie logów i raportów warto je wydzielać.

Mała tabelka na pamięć

Kategoria Przykłady Jak liczymy z grubsza
LLM GPT‑5.1, GPT‑5‑mini, embeddings, rerank
tokens_in/out × price_per_token
Infrastruktura MCP, Agents, DB, Redis, kolejki, CDN Dzielimy rachunek dostawcy przez ruch/okres
Płatności i usługi Stripe, e‑mail API, SMS, analityka Liczba zdarzeń × stawka/prowizja

Nasza cel: powiązać te kategorie z konkretnymi zdarzeniami w systemie (wywołania tooli, workflow, checkout), a nie patrzeć tylko na końcowe miesięczne sumy.

3. Gdzie zbierać dane usage: trzy warstwy

Aby liczyć cost nie „raz w miesiącu”, lecz w czasie rzeczywistym, trzeba wbudować instrumentację w kod. Miejsc są tylko trzy.

Serwer MCP: każde wywołanie narzędzia

Serwer MCP to naturalny punkt, przez który ChatGPT wywołuje Twoje narzędzia. Tutaj możemy:

  • Złapać moment rozpoczęcia/zakończenia wywołania.
  • Zmierzć duration_ms (lub latency_ms).
  • Zebrać tokeny z odpowiedzi OpenAI (jeśli MCP wywołuje nasz model) albo przynajmniej je oszacować.
  • Ustawić user_id, tenant_id, request_id/trace_id do powiązania logów.

Szkicowo zdarzenie logu tool_invocation dla GiftGenius wygląda tak:

{
  "timestamp": "2025-11-20T12:34:56Z",
  "level": "info",
  "event": "tool_invocation",
  "request_id": "abc123",
  "user_id": "user42",
  "service": "mcp-giftgenius",
  "tool_name": "suggest_gifts",
  "tokens_in": 120,
  "tokens_out": 350,
  "cost_estimate_usd": 0.045,
  "latency_ms": 320
}

Teraz to samo jako typ w TypeScript i kawałek kodu.

// types/telemetry.ts
export interface ToolInvocationLog {
  event: 'tool_invocation';
  requestId: string;
  userId?: string;
  toolName: string;
  tokensIn?: number;
  tokensOut?: number;
  costEstimateUsd?: number;
  latencyMs: number;
}
// mcp/logger.ts
export function logToolInvocation(payload: ToolInvocationLog) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    level: 'info',
    ...payload,
  }));
}

A teraz owijka wokół handlera narzędzia MCP (umownie suggest_gifts).

// mcp/tools/suggestGifts.ts
export async function handleSuggestGifts(ctx: Context, input: Input) {
  const started = Date.now();

  const llmResult = await callGiftModel(input); // tutaj wywołujemy OpenAI

  const duration = Date.now() - started;
  const { prompt_tokens, completion_tokens } = llmResult.usage ?? {};
  const costEstimate = estimateCost(prompt_tokens, completion_tokens);

  logToolInvocation({
    event: 'tool_invocation',
    requestId: ctx.requestId,
    userId: ctx.userId,
    toolName: 'suggest_gifts',
    tokensIn: prompt_tokens,
    tokensOut: completion_tokens,
    costEstimateUsd: costEstimate,
    latencyMs: duration,
  });

  return llmResult.output;
}

Nawet jeśli tokeny policzysz „na oko” przez oszacowanie długości tekstu, to już lepsze niż nic.

Poziom agenta (Agents SDK): kroki workflow

Jeśli używasz Agents SDK, agent może sam wywoływać kilka narzędzi pod rząd. Ważne jest tu logowanie kontekstu kroku: jakie zadanie agent próbuje rozwiązać.

Na przykład przy każdym wywołaniu narzędzia przez runnera agenta można dodawać pola workflow_name i step_name: „wyszukiwanie pomysłów”, „filtrowanie po budżecie”, „przygotowanie checkoutu”.

To pozwoli potem budować raporty nie tylko po narzędziach, ale i po krokach scenariusza: nagle 80% kosztu może iść na jakiś bezużyteczny „dodatkowy krok doprecyzowujący”.

Przykład małego hooka wokół agenta:

// agents/logStep.ts
export function logAgentStep(data: {
  requestId: string;
  workflow: string;
  step: string;
  toolName: string;
}) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    level: 'info',
    event: 'agent_step',
    ...data,
  }));
}

I użyć tego z runnera:

// agents/giftAgent.ts
logAgentStep({
  requestId: run.requestId,
  workflow: 'gift_selection',
  step: 'rank_candidates',
  toolName: 'rerank_gifts',
});

Commerce: checkout i pieniądze

W warstwie commerce interesują nas zdarzenia:

  • checkout_started — rozpoczęto zakup.
  • checkout_success — płatność przeszła.
  • checkout_failed — błąd z kodem/typem.

I do nich trzeba dokleić:

  • amount, currency.
  • request_id tej samej sesji co tool_invocation.

Wtedy możemy odpowiedzieć na pytanie: „Ten zakup kosztował nas N centów kosztów LLM i przyniósł M dolarów przychodu”.

Przykład prostego handlera zdarzeń checkoutu:

// api/commerce/logCheckout.ts
export function logCheckoutEvent(e: {
  type: 'checkout_started' | 'checkout_success' | 'checkout_failed';
  requestId: string;
  userId?: string;
  amountCents?: number;
  currency?: string;
  errorCode?: string;
}) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    level: 'info',
    service: 'commerce',
    ...e,
  }));
}

4. Strukturyzowane logi dla cost (powiązanie z M17)

Kluczowy punkt: żadnych „wolnych” tekstowych logów w stylu console.log("Tool suggest_gifts used 123 tokens"). Wszystko w JSON.

W module 17 już ustaliliśmy logowanie żądań jako JSON z podstawowymi polami typu request_id, user_id, tool_name itp. Teraz na to dokładamy pola cost.

Pola, które obowiązkowo powinny być w logach związanych z kosztami:

  • timestamp, level.
  • event (tool_invocation, agent_step, checkout_success itp.).
  • request_id, trace_id — aby powiązać łańcuch zdarzeń jednego workflow.
  • user_id, tenant_id — aby potem agregować po użytkownikach/firmach.
  • tool_name / service.
  • tokens_in, tokens_out, cost_estimate_usd.
  • latency_ms, success/error_code.

W przykładach będziemy nazywać pole kosztu cost_estimate_usd (koszt w dolarach amerykańskich) i trzymać się tej nazwy w kodzie i dashboardach.

Ta struktura pozwala:

  • Budować agregaty: średni cost_estimate_usd po tool_name, po user_id, po workflow.
  • Korelować „drogie” żądania z podwyższoną latencją lub błędami i decydować, co optymalizować w pierwszej kolejności.

Jeśli w M17 zrobiliście już podstawowy logger.info({...}), dodanie pól cost to nie nowy framework, tylko parę dodatkowych właściwości w obiekcie.

5. Jak w przybliżeniu liczyć koszt LLM w kodzie

Formuły są tu całkiem niestraszne. Potrzebujemy tylko przybliżonego rzędu wielkości, a nie idealnej zgodności z billingiem co do centa.

Bierzemy usage z odpowiedzi OpenAI

Gdy Twój serwer MCP wywołuje OpenAI Response API, zwykle dostaje obiekt usage:

{
  "usage": {
    "prompt_tokens": 120,
    "completion_tokens": 350,
    "total_tokens": 470
  }
}

Na jego podstawie wygodnie liczyć koszt. Różne modele mają różne ceny za 1M tokenów wejścia/wyjścia.

Najprostsza funkcja-szacująca w TypeScript:

// mcp/cost.ts
type Usage = { prompt_tokens?: number; completion_tokens?: number };

const PRICING = {
  inputPerMillion: 2.5,   // dolary za 1M tokenów wejściowych, przykład
  outputPerMillion: 10.0, // i za wyjściowe
};

export function estimateCost(
  promptTokens?: number,
  completionTokens?: number,
): number {
  const inTokens = promptTokens ?? 0;
  const outTokens = completionTokens ?? 0;

  const inputCost = (inTokens / 1_000_000) * PRICING.inputPerMillion;
  const outputCost = (outTokens / 1_000_000) * PRICING.outputPerMillion;
  return Number((inputCost + outputCost).toFixed(6)); // lekko zaokrąglamy
}

Ceny są tu przykładowe; rzeczywiste weźmiesz z aktualnego cennika OpenAI i umieścisz w konfiguracji. Ważne, że ta funkcja jest wywoływana przy każdym wywołaniu narzędzia, a wynik trafia do pola cost_estimate_usd w logu.

Jeśli usage jest niedostępny

Czasem używasz zewnętrznego LLM, który nie przysyła usage, albo potrzebujesz wstępnej kontroli przed realnym wywołaniem. Wtedy można:

  • Oszacować tokeny biblioteką typu tiktoken lub jej odpowiednikiem dla potrzebnego modelu.
  • Brać średnie wartości z historycznych logów (median_tokens_in/median_tokens_out dla narzędzia) i mnożyć je przez cenę.

Kod‑stub do oszacowania długości:

// mcp/costEstimateFallback.ts
export function roughTokenEstimate(text: string): number {
  // Zgrubna ocena: 1 token ≈ 4 znaki łacińskie
  return Math.ceil(text.length / 4);
}

To nie rocket science, ale pozwala np. nie wpuszczać do taniego planu promptu na 200000 tokenów.

6. Kluczowe metryki kosztowe

Zebrane logi to surowiec. Teraz zobaczmy, jakie agregaty są życiowo przydatne.

cost_per_tool_call

Co to jest: średni koszt jednego wywołania konkretnego narzędzia.

Po co:

  • Widać, które narzędzia są szczególnie drogie.
  • Można szukać „drogich i bezużytecznych”: wysokie avg_cost_per_call i niska konwersja do sukcesu scenariusza.

Jak liczyć z logów:

  • Bierzemy logi z event = "tool_invocation" za okres.
  • Grupujemy po tool_name.
  • Dla każdego liczymy avg(cost_estimate_usd) oraz można jeszcze p95 (95. percentyl kosztu).

cost_per_successful_task (lub cost_per_workflow)

Task/workflow — to zakończony scenariusz na poziomie użytkownika:

  • W GiftGenius może to być „dobór prezentu + pokaz kart + użytkownik zapisał N pomysłów” albo „dobór → checkout → udany zakup”.

Co robimy:

  • Przy zakończeniu workflow zapisujemy zdarzenie workflow_completed z request_id, workflow_name i flagą sukcesu.
  • Przez request_id „dociągamy” wszystkie tool_invocation tego workflow i sumujemy ich cost_estimate_usd.

W ten sposób otrzymujemy „ile kosztowało jedno udane zadanie” — klucz do zrozumienia kosztu własnego scenariusza.

cost_per_user / cost_per_tenant

Dla scenariuszy B2B często ważne jest pytanie: „Ile kosztuje nas jeden użytkownik/jedna drużyna miesięcznie?”

Liczymy:

  • Grupujemy tool_invocation i inne zdarzenia kosztowe po user_id lub tenant_id.
  • Sumujemy cost_estimate_usd za okres (dzień, miesiąc).

Potem porównujemy z ceną subskrypcji. Jeśli cost_per_user mocno zbliża się do ceny planu, pora albo podnieść cenę, albo zoptymalizować usage (o tym w kolejnym wykładzie o pricingu i eksperymentach „koszt ↔ jakość”).

7. Przykład: format tool_invocation i dashboard dla GiftGenius

Teraz zrobimy to, co było w ćwiczeniu: zaprojektujemy zdarzenie logu i minimalny dashboard po narzędziach.

Format zdarzenia tool_invocation dla GiftGenius

Wcześniej patrzyliśmy na minimalny log dla narzędzia MCP. Teraz zaprojektujmy bardziej szczegółowe zdarzenie tool_invocation, które można używać w produkcji i na dashboardach: idea ta sama, doszły pola dla serwisów, błędów i powiązania z modelami.

Najpierw — typ w TypeScript:

// telemetry/events.ts
export interface ToolInvocationEvent {
  timestamp: string;
  level: 'info' | 'error';
  event: 'tool_invocation';
  service: 'mcp-giftgenius';
  requestId: string;
  traceId?: string;
  userId?: string;
  tenantId?: string;
  toolName: string;
  modelId?: string;
  tokensIn?: number;
  tokensOut?: number;
  costEstimateUsd?: number;
  latencyMs: number;
  success: boolean;
  errorCode?: string;
}

I wygodny helper:

// telemetry/emitToolInvocation.ts
export function emitToolInvocation(e: ToolInvocationEvent) {
  console.log(JSON.stringify(e));
  // W realnym życiu: wyślemy do Logtail/Datadog/ELK itp.
}

Każdemu narzędziu (np. suggest_gifts, rerank_gifts, fetch_catalog) dodajemy wywołanie emitToolInvocation na końcu handlera (lub w bloku finally, aby log był nawet przy błędzie).

Najprostszy dashboard po narzędziach

Minimalna tabela do dashboardu (np. w Metabase / Grafana / dowolnym BI):

Kolumna Opis
tool_name
Nazwa narzędzia (suggest_gifts, checkout_create_session, …)
% ruchu
Udział wszystkich tool_invocation, które przypadły na to narzędzie
avg_cost_per_call
Średni koszt jednego wywołania (z cost_estimate_usd)
error_rate
Procent zdarzeń z success = false
avg_latency_ms
Średnia latencja
avg_revenue_per_call
Średni przychód powiązany z tym narzędziem (jeśli jest)

Wizualnie zwykle wygląda to tak: na górze tabela, na dole — para wykresów:

  • Wykres słupkowy: tool_name na osi X, avg_cost_per_call na osi Y.
  • Wykres rozrzutu: X = avg_cost_per_call, Y = error_rate lub conversion_to_checkout.

Takie wykresy pomagają szybko znaleźć kandydatów do optymalizacji: drogie, wolne i bez konwersji — najpierw tam.

Powiązać koszt z przychodem pomaga to, że logujemy checkout_* razem z request_id. Dzięki temu możemy policzyć avg_revenue_per_call jako sumę przychodu podzieloną przez liczbę wywołań narzędzia w scenariuszach, w których zaszedł checkout_success.

8. Uwzględnienie kosztów infrastruktury (bez przesady)

Z kosztami LLM wszystko jest ładne: każde wywołanie ma tokeny, można liczyć koszt wprost w logu. Infrastruktura nie jest tak prosta: masz miesięczny rachunek za Vercel, bazy, Redis itd.

Na start można pójść prostą drogą:

  1. Bierzesz sumaryczny rachunek za miesiąc dla infrastruktury (załóżmy, 200$).
  2. Dzielisz go przez liczbę workflow w miesiącu (workflow_completed) — otrzymujesz przybliżony infra_cost_per_task.
  3. Albo dzielisz przez liczbę aktywnych użytkowników — infra_cost_per_user.

Potem te liczby składamy z kosztem LLM (który policzyliśmy szczegółowo z logów) — otrzymujemy przybliżony pełny koszt własny scenariusza lub użytkownika.

Gdy aplikacja urośnie, można robić to bardziej precyzyjnie (rozkładać wydatki po serwisach i narzędziach), ale do pierwszych wersji to w zupełności wystarczy, by nie iść po omacku.

9. Mały przykład end‑to‑end dla GiftGenius

Złóżmy wszystko w mini‑historię.

Użytkownik opisuje obdarowywaną osobę, ChatGPT proponuje włączyć GiftGenius. Dalej:

  1. Widget uruchamia workflow "gift_selection".
  2. Twój backend decyduje użyć agenta LLM, aby inteligentniej dobrać prezenty.
  3. Agent wykonuje 3 kroki:
  • analyze_recipient (analiza opisu z pomocą LLM).
  • suggest_gifts (nasze narzędzie MCP).
  • rerank_gifts (dodatkowy model do poprawy listy).
  1. Użytkownik widzi karty prezentów, zapisuje kilka pomysłów.
  2. Klika „Kup”, uruchamia się ACP i checkout_create_session.
  3. Udany checkout_success z kwotą 79,00 USD.

Co zostaje w logach:

  • Trzy tool_invocation (każde z własnymi tokens_in/tokens_out, cost_estimate_usd, latencyMs).
  • Kilka agent_step z workflow = "gift_selection", step_name.
  • checkout_started i checkout_success z amount=7900, currency="USD".

Na podstawie request_id łączymy to wszystko i możemy powiedzieć:

  • Koszt LLM scenariusza: suma cost_estimate_usd trzech narzędzi, przypuśćmy 0,19$.
  • Udział infrastruktury (z agregatów) około 0,03$ na jeden workflow.
  • Razem 0,22$ kosztu własnego.
  • Przychód z transakcji — 79$ minus prowizja Stripe i inne.

To już konkretna unit economics, a nie „wydaje się, że GPT‑4 jest drogie”.

10. Typowe błędy przy pracy z instrumentacją kosztów

Błąd nr 1: liczyć tylko miesięczny rachunek i nie mieć granularności.
Bardzo kuszące jest patrzenie wyłącznie na ogólny rachunek od OpenAI/chmury. Ale bez powiązania z tool_name, user_id, workflow nie wiesz, gdzie dokładnie wydawane są pieniądze. W efekcie optymalizacja zamienia się w „ślepe obniżanie modelu” zamiast celowego usprawniania drogich scenariuszy.

Błąd nr 2: pisać dane cost w tekstowe logi bez struktury.
Linie typu "Tool suggest_gifts used 123 tokens" trudno jakościowo agregować i filtrować. W pewnym momencie dojdziesz do wniosku, że trzeba migrować na JSON — i ten przejazd będzie bolesny. Od razu rób strukturyzowane logi z polami request_id, tool_name, tokens_in/tokens_out, cost_estimate_usd.

Błąd nr 3: ignorować powiązanie cost ↔ zdarzenia commerce.
Logowanie checkout_success bez request_id i powiązania z wywołaniami narzędzi — to dobrowolna rezygnacja ze zrozumienia, które scenariusze przynoszą zysk, a które tylko palą tokeny. Nie zaniedbuj przepchnięcia request_id przez całą ścieżkę od widgetu do ACP.

Błąd nr 4: próbować zrobić „idealny” billing zamiast praktycznej oceny.
Niektóre zespoły zakopują się w próbach idealnego odtworzenia billingu OpenAI co do ostatniego tokena. W praktyce wystarczy rząd wielkości: czy scenariusz kosztuje 0,02$ czy 0,021$ — to nie jest kluczowe. Ważne, żeby nie 2$. Nie bój się używać przybliżonych estymat przez usage czy nawet grube heurystyki.

Błąd nr 5: patrzeć tylko na cost i zapominać o jakości.
Czasem, widząc ładne liczby oszczędności, chce się wszędzie przełączyć na najtańszy model. Tak można „zoptymalizować” aplikację do stanu, w którym użytkownicy przestaną z niej korzystać. Koszt trzeba rozpatrywać razem z jakością odpowiedzi i konwersją — ten tandem będzie tematem kolejnego wykładu tego modułu — o pricingu i eksperymentach „koszt ↔ jakość”.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION