Перейти к основному содержимому

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.

Решение​

  1. Приоритет. Когда chunk usage несёт costMicros, он ЯВЛЯЕТСЯ cost_micros строки — числом, которое engine прибавляет к локальному бюджету цикла (usage.costMicros хода), которое увеличивает budgets проекта, сессии и области и которое суммирует вкладка Расходы. Без него замороженная цена каталога (ADR 0042) даёт число, как всегда. Обе вещи решает одна доменная функция — custoDaChamada (domain/llm/custo-da-chamada.ts), — и три сценария использования проходят через неё; второй линейки нет.

  2. Цена строки с реальной стоимостью — НЕЯВНАЯ. стоимость ÷ токены, одно и то же значение в обоих столбцах цены (provider возвращает ОДНУ стоимость; делить её между входом и выходом значило бы выдумывать пропорцию), и price_implicit = true говорит это. Это сохраняет tokens × preço = custo (RN-044) воспроизводимым с точностью до округления целой цены, не притворяясь, что существует табличная цена, давшая это число. Это ТА ЖЕ формула (precoImplicitoPorMilhao) и ТОТ ЖЕ столбец, что в ADR 0179.

  3. То, что взял бы каталог, записывается рядом, в catalog_cost_micros, только в строках, чьё число реальное (null в остальных). Это то, что просила карточка («записывать реальную стоимость рядом с оценочной»), и это позволяет измерить строка за строкой расстояние, которое анализ 29.09 измерил в агрегате.

  4. estimated не меняет значения. Он по-прежнему говорит, были ли ТОКЕНЫ подсчитаны provider или оценены локально (RN-041). У строки с реальной стоимостью estimated = false, потому что стоимость приходит только вместе с usage provider; метка того, что СТОИМОСТЬ реальная, — price_implicit. Использовать estimated для обоих вопросов стёрло бы различие «provider сказал 0 токенов» × «provider ничего не сказал».

  5. Разрешённая модель и id генерации записываются — resolved_model_name и generation_id, читаемые из model и id фрейма ЛЮБЫМ provider диалекта (это поля самого диалекта /chat/completions, а не причуда хаба). model_name остаётся именем из КАТАЛОГА: это измерение отчётов (RN-101/RN-186) и связь с model_id; замена его на разрешённое разбила бы один и тот же binding на N строк отчёта с каждой новой датированной версией alias.

  6. Стоимость читает config-hook, а не база. extrairCustoReal в OpenAICompatibleConfig, настроенный ТОЛЬКО в OpenRouter (extrairCustoRealOpenRouter). Provider без hook игнорирует usage.cost, даже если он присутствует, — единица и значение поля принадлежат хабу, а правило Фазы 9 — особенность provider становится hook/флагом, никогда if. Новая capability в LLMProviderCapabilities не появляется: стоимость наблюдается ДЛЯ КАЖДОГО ОТВЕТА, а её отсутствие само деградирует до каталога, строка за строкой.

  7. BYOK — не реальная стоимость. При usage.is_byok === true cost — это комиссия хаба, а inference оплачивается у провайдера отдельно; записать только комиссию значило бы записать МЕНЬШЕ, чем было списано, — дефект, который закрывает этот ADR, — поэтому строка падает в каталог. Стоимость, не являющаяся конечным неотрицательным числом, тоже означает «не сказал», никогда не ноль; настоящий ноль (бесплатная модель) — это реальная стоимость.

  8. Записываются части входа и выхода (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_id null), либо вызов чата с реальной стоимостью (model_id заполнен, catalog_cost_micros не null); Jev не записывает catalog_cost_micros, потому что у него нет цены каталога.
  • Anthropic остаётся вне частей. Он сообщает кэш в собственном протоколе (cache_read_input_tokens), который его адаптер не читает; его строки записывают null в обоих столбцах. Ни один другой provider не меняется.
  • То, что открывают разрешённая модель и id, — сверка списания по строке с GET /generation?id=, переоценка по датированной версии, — остаётся построить; здесь только записывается.