ADR 0188 — Реальная стоимость, которую возвращает provider, становится числом metering
Статус
Accepted. 2026-09-30 (AT-270 и AT-272, история HS-070, эпик EP-030, раунд 34;
решение владельца от 30.09: «реальная стоимость становится числом» — когда provider
возвращает то, что списал, именно это значение записывается и показывается, с
estimated = false; цена каталога остаётся только там, где реальной стоимости нет).
Дополняет
ADR 0042
(замороженная и проверяемая цена) и
ADR 0076 (provider как
измерение расходов), не редактируя их: замороженная цена по-прежнему существует и
по-прежнему является числом каждого вызова, ответ которого не сообщает стоимость; меняется
то, какой из двух источников побеждает, когда существуют оба. Распространяет на ЧАТ
схему, которую ADR 0179 ввёл
для стоимости решения о выборе инструмента Jev, — ТОТ ЖЕ столбец
price_implicit и ТУ ЖЕ функцию precoImplicitoPorMilhao, — вместо
второго способа сказать «эта стоимость реальная».
Контекст
Измерено в коде dev 2026-09-30, до изменения:
- Стоимость, записываемая в
token_usage.cost_micros, бралась из ОДНОГО места, повторённого в трёх сценариях использования —RunLlmTurnUseCase(276 из 305 строк реального использования 29.09),StreamLlmTurnUseCase(28) иSendChatMessageUseCase(1):calculateCostMicros(tokens, preço de models). Никакой другой путь не записывает metering (RecordLlmUsageUseCase— единственный, и он лишь передаёт число). OpenAICompatibleProvider.chatчитал из фреймаusageтолькоprompt_tokensиcompletion_tokens. Он отбрасывалusage.cost(стоимость, которую списал OpenRouter, в USD),model(модель, которая ОБСЛУЖИЛА, — alias~deepseek/deepseek-flash-latestразрешается в датированную версию) иid(gen-…, идентификатор, который принимаетGET /api/v1/generation?id=). Находка анализа реального использования (openai-compatible-provider.ts:237-245) воспроизводится байт в байт вdev.- Расстояние измерено, а не предположено: реальное использование 29.09 составило US$ 0,314259
по замороженной цене (20 000 / 600 000 micros за миллион), а тестовый ключ
зарегистрировал US$ 0,5807 за день (1,85×). При переоценке по НИЖЕЛЕЖАЩЕМУ provider,
который
upstream_providerуже записывал, потолок без кэша даёт ≈ US$ 0,917. Цена каталога хаба — это цена ОДНОГО маршрута; хаб берёт плату за маршрут, который использовал. usage.costуже был источником расходов для всех платных измерений, сделанных против OpenRouter (docs/explanation/medicao-do-idioma.md,medicao-do-jev.md,scripts/idioma/validar.ts): поле существует в реальном ответе/chat/completions, и именно по нему суммировались их расходы, никогда не по счётчику/key.
Решение
-
Приоритет. Когда chunk
usageнесётcostMicros, он ЯВЛЯЕТСЯcost_microsстроки — числом, которое engine прибавляет к локальному бюджету цикла (usage.costMicrosхода), которое увеличивает budgets проекта, сессии и области и которое суммирует вкладка Расходы. Без него замороженная цена каталога (ADR 0042) даёт число, как всегда. Обе вещи решает одна доменная функция —custoDaChamada(domain/llm/custo-da-chamada.ts), — и три сценария использования проходят через неё; второй линейки нет. -
Цена строки с реальной стоимостью — НЕЯВНАЯ.
стоимость ÷ токены, одно и то же значение в обоих столбцах цены (provider возвращает ОДНУ стоимость; делить её между входом и выходом значило бы выдумывать пропорцию), иprice_implicit = trueговорит это. Это сохраняетtokens × preço = custo(RN-044) воспроизводимым с точностью до округления целой цены, не притворяясь, что существует табличная цена, давшая это число. Это ТА ЖЕ формула (precoImplicitoPorMilhao) и ТОТ ЖЕ столбец, что в ADR 0179. -
То, что взял бы каталог, записывается рядом, в
catalog_cost_micros, только в строках, чьё число реальное (nullв остальных). Это то, что просила карточка («записывать реальную стоимость рядом с оценочной»), и это позволяет измерить строка за строкой расстояние, которое анализ 29.09 измерил в агрегате. -
estimatedне меняет значения. Он по-прежнему говорит, были ли ТОКЕНЫ подсчитаны provider или оценены локально (RN-041). У строки с реальной стоимостьюestimated = false, потому что стоимость приходит только вместе сusageprovider; метка того, что СТОИМОСТЬ реальная, —price_implicit. Использоватьestimatedдля обоих вопросов стёрло бы различие «provider сказал 0 токенов» × «provider ничего не сказал». -
Разрешённая модель и id генерации записываются —
resolved_model_nameиgeneration_id, читаемые изmodelиidфрейма ЛЮБЫМ provider диалекта (это поля самого диалекта/chat/completions, а не причуда хаба).model_nameостаётся именем из КАТАЛОГА: это измерение отчётов (RN-101/RN-186) и связь сmodel_id; замена его на разрешённое разбила бы один и тот же binding на N строк отчёта с каждой новой датированной версией alias. -
Стоимость читает config-hook, а не база.
extrairCustoRealвOpenAICompatibleConfig, настроенный ТОЛЬКО в OpenRouter (extrairCustoRealOpenRouter). Provider без hook игнорируетusage.cost, даже если он присутствует, — единица и значение поля принадлежат хабу, а правило Фазы 9 — особенность provider становится hook/флагом, никогдаif. Новая capability вLLMProviderCapabilitiesне появляется: стоимость наблюдается ДЛЯ КАЖДОГО ОТВЕТА, а её отсутствие само деградирует до каталога, строка за строкой. -
BYOK — не реальная стоимость. При
usage.is_byok === truecost— это комиссия хаба, а inference оплачивается у провайдера отдельно; записать только комиссию значило бы записать МЕНЬШЕ, чем было списано, — дефект, который закрывает этот ADR, — поэтому строка падает в каталог. Стоимость, не являющаяся конечным неотрицательным числом, тоже означает «не сказал», никогда не ноль; настоящий ноль (бесплатная модель) — это реальная стоимость. -
Записываются части входа и выхода (AT-272, RN-666):
cached_input_tokens(usage.prompt_tokens_details.cached_tokens) иreasoning_tokens(usage.completion_tokens_details.reasoning_tokens), читаемые базой для любого provider диалекта, потому что это поля самой OpenAI. Это ЧАСТИinput_tokens/output_tokens, никогда не прибавляемые к ним, и они не пересчитывают стоимость: при реальной стоимости скидка кэша уже в числе; без неё у каталога нет цены кэша для применения, а выдумать её было бы ещё одной оценкой.null— это «не сказал», отличное от 0. Измерение (medir-execucao.ts) начинает говорить по каждому агенту, какая часть входа была прочитанным кэшем — только по вызовам, которые сообщили, и в скольких, — и в скольких стоимость реальная. Интервал, который анализ 29.09 оставил как гипотезу (между US$ 0,58 и 0,92, в зависимости от кэша), становится измеримым в новых вызовах; 305 старых остаются без этой информации.
Последствия
- Число на вкладке Расходы, в budgets и в локальном бюджете цикла становится
тем, что списывает OpenRouter, в вызовах, которые он измеряет. Один и тот же отчёт суммирует
реальные строки и оценочные;
price_implicitразличает их построчно. - Budget может «подскочить». Budget, откалиброванный по цене каталога, видит, начиная отсюда, расход до ~1,85× больше за те же вызовы (замер 29.09): пороги 70/90/100% достигаются раньше. Это запрошенное поведение — бюджет становится бюджетом денег, а не оценки, — и это объявлено для тех, кто калибровал потолок раньше.
- Экран всё ещё говорит «оценочно». Подзаголовок вкладки Расходы участника
(RN-101) утверждает, что стоимость оценочная. Как экран различает реальное и
оценочное — метка на строку, пропорция или лишь замена фразы, — это
ожидающее продуктовое решение владельца; ни один экран здесь не затронут, и api
пока не раскрывает
price_implicitни в одном маршруте. - Доказательство без ключа. На этой машине нет учётных данных OpenRouter: доказательство —
это ЗАПИСАННЫЙ ОТВЕТ в наборе контрактных тестов
(
openrouter-provider.contract.spec.ts, с формойusage, которую прочитали платные измерения), а ручной smoke (openrouter-provider.smoke.spec.ts) начинает требоватьprice_implicitиgen-…при наличии учётных данных. Значениеcostпри BYOK — это ЧТЕНИЕ документации, а не измерение. - Один столбец, два автора.
price_implicitродился в migration 0066 (Jev); это решение лишь добавляет второго его писателя, а migration 0067 создаёт только три новых столбца. Строка сprice_implicit = true— это либо Jev (model_name = typesafe/jev-1.13,model_idnull), либо вызов чата с реальной стоимостью (model_idзаполнен,catalog_cost_microsне null); Jev не записываетcatalog_cost_micros, потому что у него нет цены каталога. - Anthropic остаётся вне частей. Он сообщает кэш в собственном протоколе
(
cache_read_input_tokens), который его адаптер не читает; его строки записываютnullв обоих столбцах. Ни один другой provider не меняется. - То, что открывают разрешённая модель и id, — сверка списания по строке
с
GET /generation?id=, переоценка по датированной версии, — остаётся построить; здесь только записывается.