JavaRush /Курси /ChatGPT Apps /Контроль витрат і cost-інструментація

Контроль витрат і cost-інструментація

ChatGPT Apps
Рівень 19 , Лекція 0
Відкрита

1. Чому «працює» ≠ «окупається»

LLM‑застосунки мають важливу особливість: окрім фіксованих витрат на хостинг, у них часто зʼявляються змінні витрати на виконання окремих запитів, повʼязані з викликами моделей.

Важливо розрізняти два сценарії:

  • коли модель працює на боці ChatGPT (користувач спілкується з вашим App у ChatGPT, а той викликає mcp-tools) — за токени платить користувач зі своєї підписки ChatGPT;
  • коли ваш бекенд/MCP‑сервер сам викликає OpenAI API або інші LLM‑сервіси — за ці токени платите вже ви.

Саме в другому випадку у вас зʼявляються класичні змінні LLM‑витрати. Вони залежать від кількості й «важкості» (tokens_in/tokens_out) запитів.

Класичний сценарій:

  1. Ви радісно розгортаєте GiftGenius у продакшні: усе літає, користувачі задоволені.
  2. За місяць приходить рахунок за OpenAI + хмару + комісії Stripe. І раптом зʼясовується, що «успішне зростання» насправді означає «ми платимо за кожен подарунок більше, ніж заробляємо з продажу».

Підхід FinOps підказує: вартість — це така сама метрика, як затримка чи частота помилок. Її потрібно логувати, агрегувати й ухвалювати рішення на основі цих даних, а не «вгадувати в Excel».

Мета цієї лекції — щоб ви могли відповідати на запитання на кшталт:

  • «Скільки коштував ось цей конкретний підбір подарунка для користувача user42
  • «Скільки грошей за цей тиждень “спалив” інструмент suggest_gifts і скільки він при цьому приніс замовлень?»

І щоб відповіді бралися не «зі стелі», а з логів і метрик.

2. Структура витрат ChatGPT App

Почнімо з карти витрат. Без неї все інше перетворюється на хаотичне збирання чисел.

LLM‑витрати (змінні)

Це все, що повʼязане з викликами моделей із вашого бекенду:

  • Виклики OpenAI‑моделей із MCP‑сервера або агентів: GPT-5.1 / GPT-5-mini / embeddings / rerank / vision / TTS/STT тощо.
  • Додаткові моделі: reranking для пошуку, ембеддинги для рекомендацій, генерація зображень.

Важливий нюанс: якщо ви будуєте інтерфейс через Apps SDK і використовуєте лише вбудовану модель ChatGPT, ви не платите за токени — платить користувач (через свою підписку ChatGPT). Але щойно ваш MCP‑сервер сам починає викликати OpenAI API (Agents, Responses API, embeddings тощо), токени вже списуються з вашого рахунку.

Базова ідея проста: вартість таких викликів пропорційна tokens_in і tokens_out, помноженим на ціну за токен.

Виклик MCP‑tool сам по собі безплатний для розробника з погляду токенів. Витрати зʼявляються лише там, де в його обробнику ви вирішуєте викликати OpenAI API або іншу LLM.

Інфраструктура

Це все «залізо» і сервіси довкола:

  • MCP‑сервери: Vercel / AWS / GCP / bare metal.
  • Агенти (якщо працюють як окремі сервіси).
  • Бази даних: Postgres/MySQL, векторні БД, S3/обʼєктні сховища.
  • Кеші: Redis/KeyDB.
  • Черги та воркери: наприклад, для фонової генерації, перерахунку фідів тощо.

Ці витрати частіше фіксовані за місяць (або ступінчасто фіксовані). Тому їх зазвичай рахують за агрегованими даними з витрат на хмарні сервіси, а не для кожного запиту.

Платіжні та зовнішні сервіси

У GiftGenius є ACP і Stripe, а отже зʼявляються:

  • Комісії за кожен успішний платіж (Stripe — на рівні кількох відсотків + фіксована частина).
  • Втрати на шахрайство й чарджбеки.
  • Вартість зовнішніх API: e‑mail / SMS / push‑сповіщення, додаткова аналітика тощо.

На старті це копійки, але зі зростанням вони починають відчуватися. Тож хоча б на рівні логів і звітів ці витрати корисно виділяти окремо.

Невелика таблиця для памʼяті

Категорія Приклади Як приблизно рахуємо
LLM GPT‑5.1, GPT‑5‑mini, embeddings, rerank
tokens_in/out × price_per_token
Інфраструктура MCP, Agents, DB, Redis, черги, CDN Ділимо рахунок провайдера на трафік/період
Платежі та сервіси Stripe, e‑mail API, SMS, аналітика Кількість подій × тариф/комісію

Наша мета: привʼязати ці категорії до конкретних подій у системі (tool‑виклики, workflow, checkout), а не дивитися лише на підсумкові місячні суми.

3. Де знімати дані usage: три рівні

Щоб рахувати cost не «раз на місяць», а в реальному часі, потрібно вбудувати інструментацію в код. Точок для цього — всього три.

MCP‑сервер: кожен виклик інструмента

MCP‑сервер — природна точка, через яку ChatGPT викликає ваші tools. Тут ми можемо:

  • Зловити момент початку й завершення виклику.
  • Виміряти duration_ms (або latency_ms).
  • Зібрати токени з відповіді OpenAI (якщо MCP викликає нашу модель) або принаймні оцінити їх.
  • Додати user_id, tenant_id, request_id/trace_id для звʼязування логів.

Схематично лог‑подія tool_invocation для GiftGenius виглядає так:

{
  "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
}

А тепер те саме — у вигляді TypeScript‑типу та шматка коду.

// 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,
  }));
}

А тепер — обгортка навколо обробника MCP‑інструмента (умовно suggest_gifts).

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

  const llmResult = await callGiftModel(input); // тут викликаємо 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;
}

Навіть якщо токени порахувати «на око» — просто через оцінку довжини тексту, — це все одно краще, ніж нічого.

Рівень агента (Agents SDK): кроки workflow

Якщо ви використовуєте Agents SDK, агент може сам викликати кілька tools поспіль. Тут важливо логувати контекст кроку: яке завдання намагається розвʼязати агент.

Наприклад, під час кожного tool‑виклику з агентного раннера можна додавати поля workflow_name і step_name: «пошук ідей», «фільтрація за бюджетом», «підготовка checkout».

Це дасть змогу згодом будувати звіти не лише за інструментами, а й за кроками сценарію. Раптом 80 % вартості йде на якийсь непотрібний «додатковий уточнювальний крок».

Приклад невеликого «хука» навколо агента:

// 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,
  }));
}

І як використати це з раннера:

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

Commerce: checkout і гроші

У шарі commerce нас цікавлять події:

  • checkout_started — покупку розпочато.
  • checkout_success — оплата пройшла.
  • checkout_failed — сталася помилка (з кодом/типом).

І до них потрібно «приклеїти»:

  • amount, currency.
  • request_id тієї самої сесії, що й tool_invocation.

Тоді ви зможете відповісти на запитання: «Ця покупка коштувала нам N центів LLM‑витрат і принесла M доларів виручки».

Приклад простого обробника подій checkout:

// 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. Структуровані логи для cost (звʼязок з М17)

Ключовий момент: жодних «вільних» текстових логів на кшталт console.log("Tool suggest_gifts used 123 tokens"). Усе — в JSON.

У модулі 17 ми вже домовилися логувати запити у вигляді JSON із базовими полями на кшталт request_id, user_id, tool_name тощо. Тепер поверх цього додамо cost‑поля.

Поля, які обовʼязково мають бути в логах, повʼязаних із витратами:

  • timestamp, level.
  • event (tool_invocation, agent_step, checkout_success тощо).
  • request_id, trace_id — щоб звʼязати ланцюжок подій одного workflow.
  • user_id, tenant_id — щоб потім агрегувати за користувачами/компаніями.
  • tool_name / service.
  • tokens_in, tokens_out, cost_estimate_usd.
  • latency_ms, success/error_code.

У прикладах називатимемо поле вартості cost_estimate_usd (вартість у доларах США) і дотримуватимемося цієї назви в коді та дашбордах.

Саме така структура дає змогу:

  • Будувати агрегати: середній cost_estimate_usd за tool_name, за user_id, за workflow.
  • Повʼязувати «дорогі» запити з підвищеною затримкою або помилками й вирішувати, що оптимізувати в першу чергу.

Якщо ви вже в М17 зробили базовий logger.info({...}), то додавання cost‑полів — це не новий фреймворк, а просто кілька додаткових властивостей в обʼєкті.

5. Як приблизно рахувати LLM‑cost у коді

Формули тут зовсім не страшні. Нам потрібен лише приблизний порядок величин, а не ідеальна відповідність білінгу — до останнього цента.

Беремо usage з OpenAI‑відповіді

Коли ваш MCP‑сервер викликає OpenAI Response API, він зазвичай отримує обʼєкт usage:

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

За ним зручно рахувати вартість. Різні моделі мають різні ціни за 1M токенів входу/виходу.

Найпростіша функція‑оцінювач на TypeScript:

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

const PRICING = {
  inputPerMillion: 2.5,   // доларів за 1M вхідних токенів, приклад
  outputPerMillion: 10.0, // і за вихідні
};

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)); // трохи округлюємо
}

Ціни тут приблизні; реальні ви візьмете з актуальних тарифів OpenAI й покладете в конфіг. Важливо інше: цю функцію викликають під час кожного tool‑виклику, а результат потрапляє в поле cost_estimate_usd у логу.

Якщо usage недоступний

Іноді ви використовуєте сторонню LLM, яка не надсилає usage. Або вам потрібен попередній контроль ще до реального виклику. Тоді можна:

  • Оцінювати токени за допомогою бібліотеки типу tiktoken або її аналога для потрібної моделі.
  • Брати середні значення з історичних логів (median_tokens_in/median_tokens_out для інструмента) і множити їх на ціну.

Код‑заглушка для оцінки довжини:

// mcp/costEstimateFallback.ts
export function roughTokenEstimate(text: string): number {
  // Груба оцінка: 1 токен ≈ 4 символи латиниці
  return Math.ceil(text.length / 4);
}

Це не rocket science, але так ви, наприклад, зможете не пускати в дешевий тариф промпт на 200000 токенів.

6. Ключові cost‑метрики

Зібрані логи — це сировина. Тепер подивімося, які агрегати з них справді життєво корисні.

cost_per_tool_call

Що це: середня вартість одного виклику конкретного інструмента.

Навіщо:

  • Видно, які інструменти особливо дорогі.
  • Можна шукати «дорогі й марні»: високий avg_cost_per_call і низька конверсія в успіх сценарію.

Як рахувати за логами:

  • Беремо логи з event = "tool_invocation" за період.
  • Групуємо за tool_name.
  • Для кожного рахуємо avg(cost_estimate_usd) і, за потреби, p95 (95‑й перцентиль вартості).

cost_per_successful_task (або cost_per_workflow)

Task/workflow — це завершений сценарій на рівні користувача:

  • У GiftGenius це може бути «підбір подарунка + показ карток + користувач зберіг N ідей» або «підбір → checkout → успішна покупка».

Що робимо:

  • Під час завершення workflow пишемо подію workflow_completed з request_id, workflow_name і прапорцем успішності.
  • Через request_id «підтягуємо» всі tool_invocation цього workflow і підсумовуємо їх cost_estimate_usd.

У результаті отримуємо «скільки коштувало одне успішне завдання» — ключ до розуміння собівартості сценарію.

cost_per_user / cost_per_tenant

Для B2B‑сценаріїв часто важливе питання: «Скільки коштує нам один користувач/одна команда на місяць?»

Рахуємо:

  • Групуємо tool_invocation та інші cost‑події за user_id або tenant_id.
  • Підсумовуємо cost_estimate_usd за період (день, місяць).

Потім порівнюємо з ціною підписки. Якщо cost_per_user надто наближається до ціни тарифу, пора або піднімати ціну, або оптимізувати usage (про це поговоримо в наступній лекції — про pricing і експерименти «вартість ↔ якість»).

7. Приклад: формат tool_invocation і дашборд для GiftGenius

Тепер зробімо те, що було у вправі з плану: спроєктуємо лог‑подію і мінімальний дашборд за tools.

Формат події tool_invocation для GiftGenius

Раніше ми дивилися на мінімальний лог для MCP‑інструмента. Тепер спроєктуймо детальнішу подію tool_invocation, яку вже можна використовувати в продакшні та на дашбордах. Ідея та сама — просто додалися поля для сервісів, помилок і звʼязку з моделями.

Спочатку — тип у 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;
}

І зручна допоміжна функція:

// telemetry/emitToolInvocation.ts
export function emitToolInvocation(e: ToolInvocationEvent) {
  console.log(JSON.stringify(e));
  // У реальному житті: надішлемо в Logtail/Datadog/ELK тощо.
}

Кожному інструменту (наприклад, suggest_gifts, rerank_gifts, fetch_catalog) ми додаємо виклик emitToolInvocation наприкінці handlerʼа (або в finally-блоці, щоб лог зʼявився навіть у разі помилки).

Найпростіший дашборд за інструментами

Мінімальна таблиця для дашборда (наприклад, у Metabase / Grafana / будь‑якому BI):

Стовпець Опис
tool_name
Імʼя інструмента (suggest_gifts, checkout_create_session, …)
% трафіка
Частка всіх tool_invocation, які припали на цей інструмент
avg_cost_per_call
Середня вартість одного виклику (з cost_estimate_usd)
error_rate
Відсоток подій із success = false
avg_latency_ms
Середня затримка
avg_revenue_per_call
Середня виручка, асоційована з цим інструментом (якщо є)

Візуально це зазвичай виглядає так: зверху — таблиця, знизу — кілька графіків:

  • Стовпчиковий графік: tool_name по осі X, avg_cost_per_call по осі Y.
  • Діаграма розсіювання: X = avg_cost_per_call, Y = error_rate або conversion_to_checkout.

Такі графіки допомагають швидко знайти кандидатів на оптимізацію: дорого, повільно й не дає конверсії — починайте звідти.

Повʼязати cost із revenue допомагає те, що ми логуємо checkout_* разом із request_id. Тоді ми можемо порахувати avg_revenue_per_call як суму виручки, поділену на кількість викликів інструмента в сценаріях, де стався checkout_success.

8. Облік інфраструктурних витрат (без фанатизму)

З LLM‑витратами все доволі прозоро: у кожного виклику є токени, тож можна прямо в логах оцінювати вартість. З інфраструктурою складніше: у вас є місячний рахунок за Vercel, бази, Redis тощо — і він не «привʼязаний» до одного запиту.

На старті можна піти простішим шляхом:

  1. Берете сумарний рахунок за місяць по інфраструктурі (скажімо, 200 $).
  2. Ділите його на кількість workflow за місяць (workflow_completed) — отримуєте приблизний infra_cost_per_task.
  3. Або ділите на кількість активних користувачів — infra_cost_per_user.

Потім додаєте ці числа до LLM‑cost (який ми порахували детально за логами) — і отримуєте приблизну повну собівартість сценарію або користувача.

Коли застосунок виросте, можна буде зробити тонше (розкидати витрати за сервісами й інструментами). Але для перших версій цього цілком достатньо, щоб не діяти навмання.

9. Невеликий приклад end‑to‑end для GiftGenius

Зберімо все в одну мініісторію.

Користувач описує отримувача подарунка, ChatGPT пропонує увімкнути GiftGenius. Далі:

  1. Віджет запускає workflow "gift_selection".
  2. Ваш бекенд вирішує використати LLM‑агента, щоб інтелектуальніше підібрати подарунки.
  3. Агент робить 3 кроки:
  • analyze_recipient (аналіз опису за допомогою LLM).
  • suggest_gifts (наш MCP‑інструмент).
  • rerank_gifts (додаткова модель для поліпшення списку).
  1. Користувач бачить картки подарунків і зберігає кілька ідей.
  2. Натискає «Купити», запускається ACP і checkout_create_session.
  3. Успішний checkout_success із сумою 79.00 USD.

Що в нас залишається в логах:

  • Три tool_invocation (кожен зі своїми tokens_in/tokens_out, cost_estimate_usd, latencyMs).
  • Кілька agent_step із workflow = "gift_selection", step_name.
  • checkout_started і checkout_success із amount=7900, currency="USD".

За request_id ми звʼязуємо все це й можемо сказати:

  • LLM‑вартість сценарію: сума cost_estimate_usd трьох інструментів, припустімо 0.19 $.
  • Частка інфраструктури (з агрегатів) — приблизно 0.03 $ на один workflow.
  • Разом — 0.22 $ собівартості.
  • Виручка за транзакцією — 79 $ мінус комісія Stripe та інше.

Це вже конкретна юніт‑економіка, а не «здається, GPT‑4 — це дорого».

10. Типові помилки під час роботи з cost‑інструментацією

Помилка №1: рахувати лише місячний рахунок і не мати гранулярності.
Дуже спокусливо дивитися тільки на загальний рахунок від OpenAI/хмари. Але без звʼязку з tool_name, user_id, workflow ви не знаєте, де саме витрачаються гроші. У підсумку оптимізація перетворюється на «сліпе здешевлення моделі» замість точкового поліпшення дорогих сценаріїв.

Помилка №2: записувати cost‑дані в текстові логи без структури.
Рядки на кшталт "Tool suggest_gifts used 123 tokens" неможливо якісно агрегувати й фільтрувати. У якийсь момент ви зрозумієте, що треба мігрувати на JSON, і цей переїзд буде болючим. Тому одразу робіть структуровані логи з полями request_id, tool_name, tokens_in/tokens_out, cost_estimate_usd.

Помилка №3: ігнорувати звʼязок cost ↔ commerce‑події.
Логувати checkout_success без request_id і звʼязку з tool‑викликами — означає добровільно відмовитися від розуміння, які сценарії приносять прибуток, а які лише «зʼїдають» токени. Не лінуйтеся протягнути request_id через увесь шлях — від віджета до ACP.

Помилка №4: намагатися зробити «ідеальний» білінг замість практичної оцінки.
Деякі команди «закопуються» в спробах ідеально відтворити білінг OpenAI до останнього токена. У реальності достатньо порядку величин: якщо сценарій коштує 0.02 $ чи 0.021 $, це не принципово. Важливо, що це не 2 $. Не бійтеся використовувати приблизні оцінки через usage або навіть грубі евристики.

Помилка №5: дивитися лише на cost і забувати про якість.
Іноді, побачивши приємні цифри економії, хочеться всюди перемкнутися на найдешевшу модель. Так легко «оптимізувати» застосунок до стану, коли користувачі перестануть ним користуватися. Вартість слід розглядати разом із якістю відповідей і конверсією — цей звʼязок буде темою наступної лекції цього ж модуля: про pricing і експерименти «вартість ↔ якість».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ